@xfey/tutti 0.1.94 → 0.1.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/collaboration-state/index.d.ts +2 -0
  2. package/dist/collaboration-state/index.js +1 -0
  3. package/dist/collaboration-state/messages.js +15 -0
  4. package/dist/collaboration-state/run-recording.js +18 -1
  5. package/dist/collaboration-state/run-results.js +6 -0
  6. package/dist/collaboration-state/task-retry.d.ts +16 -0
  7. package/dist/collaboration-state/task-retry.js +136 -0
  8. package/dist/control-plane/command-recovery.js +9 -1
  9. package/dist/control-plane/index.d.ts +7 -3
  10. package/dist/control-plane/index.js +68 -6
  11. package/dist/control-plane/run-retry.d.ts +19 -0
  12. package/dist/control-plane/run-retry.js +103 -0
  13. package/dist/control-plane/run-start.d.ts +2 -1
  14. package/dist/control-plane/run-start.js +3 -0
  15. package/dist/control-plane/scratchpad-refresh.js +3 -4
  16. package/dist/control-plane/types.d.ts +12 -0
  17. package/dist/control-plane/workflows/openai.d.ts +1 -0
  18. package/dist/control-plane/workflows/openai.js +86 -15
  19. package/dist/control-plane/worklist-terminal-feedback.js +3 -0
  20. package/dist/prompt-templates/index.d.ts +1 -1
  21. package/dist/prompt-templates/index.js +4 -3
  22. package/dist/providers/openai/app-server/durable-session-smoke-cli.d.ts +2 -0
  23. package/dist/providers/openai/app-server/durable-session-smoke-cli.js +229 -0
  24. package/dist/providers/openai/app-server/read-only-procedure.d.ts +10 -0
  25. package/dist/providers/openai/app-server/read-only-procedure.js +42 -13
  26. package/dist/providers/openai/app-server/session-store.d.ts +74 -0
  27. package/dist/providers/openai/app-server/session-store.js +303 -0
  28. package/dist/providers/openai/app-server/workspace-write-run.d.ts +17 -4
  29. package/dist/providers/openai/app-server/workspace-write-run.js +92 -28
  30. package/dist/providers/openai/codex-app-server.d.ts +1 -0
  31. package/dist/providers/openai/codex-app-server.js +2 -0
  32. package/dist/run-pipeline/openai.d.ts +1 -0
  33. package/dist/run-pipeline/openai.js +15 -43
  34. package/dist/run-pipeline/task-run-invocation.js +354 -93
  35. package/dist/server-shell/http/routes/project-api/execution-routes.js +32 -2
  36. package/dist/server-shell/http/routes/project-api/invalidations.d.ts +2 -1
  37. package/dist/server-shell/http/routes/project-api/invalidations.js +13 -0
  38. package/dist/server-shell/http/routes/project-api/openapi-execution-routes.d.ts +21 -0
  39. package/dist/server-shell/http/routes/project-api/openapi-execution-routes.js +12 -1
  40. package/dist/server-shell/http/routes/project-api/openapi.d.ts +7 -0
  41. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +1 -0
  42. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  43. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  44. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +5 -0
  45. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +1 -0
  46. package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +1 -0
  47. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +4 -1
  48. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +1 -0
  49. package/node_modules/@tutti/shared/dist/schemas/api/work-items.d.ts +4 -0
  50. package/node_modules/@tutti/shared/dist/schemas/api/work-items.js +3 -0
  51. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +33 -0
  52. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +35 -1
  53. package/package.json +1 -1
  54. package/prompts/runs/README.md +11 -16
  55. package/prompts/runs/task-execute.md +86 -0
  56. package/prompts/runs/task-finalize.md +11 -0
  57. package/prompts/runs/task-recovery.md +15 -0
  58. package/prompts/runs/task-result-contract.md +64 -0
  59. package/web/assets/{homepage-motion-scene-CLTkixIk.js → homepage-motion-scene-BFo4r4nz.js} +1 -1
  60. package/web/assets/index-CNIl4TF8.js +69 -0
  61. package/web/assets/index-quWLFJgD.css +1 -0
  62. package/web/index.html +2 -2
  63. package/prompts/runs/task-continuation.md +0 -201
  64. package/prompts/runs/task-retry.md +0 -209
  65. package/prompts/runs/task-run.md +0 -188
  66. package/web/assets/index-BTNYElB-.js +0 -69
  67. package/web/assets/index-Deu_eF5K.css +0 -1
@@ -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,136 @@
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
52
+ .prepare("SELECT * FROM tasks WHERE id = ?")
53
+ .get(input.task_id);
54
+ if (taskRow === undefined) {
55
+ return { disposition: { kind: "task_not_failed" } };
56
+ }
57
+ if (taskRow.latest_run_result_id !== input.expected_latest_run_result_id) {
58
+ return {
59
+ disposition: {
60
+ kind: "latest_run_result_changed",
61
+ ...(taskRow.latest_run_result_id === null
62
+ ? {}
63
+ : { latest_run_result_id: taskRow.latest_run_result_id }),
64
+ },
65
+ };
66
+ }
67
+ if (taskRow.status === "running" && taskRow.current_activity_ref !== null) {
68
+ return {
69
+ disposition: {
70
+ kind: "already_running",
71
+ activity_ref: taskRow.current_activity_ref,
72
+ },
73
+ };
74
+ }
75
+ if (taskRow.status !== "failed") {
76
+ return { disposition: { kind: "task_not_failed" } };
77
+ }
78
+ const runResultRow = tx
79
+ .prepare("SELECT * FROM run_results WHERE id = ?")
80
+ .get(input.expected_latest_run_result_id);
81
+ if (runResultRow === undefined || runResultRow.task_id !== input.task_id) {
82
+ return { disposition: { kind: "latest_run_result_changed" } };
83
+ }
84
+ const result = parseJson(runResultRow.result_json, {});
85
+ if (result.retry_activity_ref !== undefined) {
86
+ return {
87
+ disposition: {
88
+ kind: "retry_already_started",
89
+ activity_ref: result.retry_activity_ref,
90
+ },
91
+ };
92
+ }
93
+ if (!failedResultCanRetry(runResultRow, result)) {
94
+ return { disposition: { kind: "retry_requires_resolution" } };
95
+ }
96
+ const updatedAt = nowIso(input.now);
97
+ tx.prepare("UPDATE run_results SET result_json = ? WHERE id = ?").run(stringifyJson({
98
+ ...result,
99
+ retry_action: "retried",
100
+ retry_activity_ref: input.activity_ref,
101
+ retried_at: updatedAt,
102
+ }), input.expected_latest_run_result_id);
103
+ tx.prepare(`
104
+ UPDATE tasks
105
+ SET status = 'running',
106
+ current_activity_ref = ?,
107
+ updated_at = ?
108
+ WHERE id = ?
109
+ AND status = 'failed'
110
+ `).run(input.activity_ref, updatedAt, input.task_id);
111
+ const detail = readTaskDetailProjection(tx, input.task_id);
112
+ if (detail === null) {
113
+ throw new Error("Task retry could not read task detail.");
114
+ }
115
+ 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);
116
+ recordReadNotificationEvent(tx, {
117
+ scope: "worklist",
118
+ source_kind: "task_run_started",
119
+ source_ref: input.task_id,
120
+ occurred_at: updatedAt,
121
+ });
122
+ const task = readTaskProjection(tx, input.task_id);
123
+ if (task === null) {
124
+ throw new Error("Task retry did not produce a readable task.");
125
+ }
126
+ return {
127
+ disposition: {
128
+ kind: "accepted",
129
+ task_id: input.task_id,
130
+ activity_ref: input.activity_ref,
131
+ retried_run_result_id: input.expected_latest_run_result_id,
132
+ },
133
+ };
134
+ });
135
+ }
136
+ //# 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";
@@ -18,6 +18,7 @@ export declare class Phase5ControlPlane {
18
18
  private readonly runLifecycle;
19
19
  private readonly now;
20
20
  private readonly scratchpadAutoRefreshScheduler;
21
+ private pendingWorklistCompletionContextSyncTimer;
21
22
  private readonly pendingReferenceSummaryPaths;
22
23
  private readonly referenceSummaryWaiters;
23
24
  private pendingAllReferenceSummaries;
@@ -28,6 +29,7 @@ export declare class Phase5ControlPlane {
28
29
  constructor(options: Phase5ControlPlaneOptions);
29
30
  getExecutionStatus(): ExecutionStatusProjection;
30
31
  runSchedulerNow(payload?: RunSchedulerNowPayload): ControlPlaneCommandResult<RunSchedulerNowDisposition, never>;
32
+ retryTask(taskId: TaskId, payload: RetryTaskPayload): ControlPlaneCommandResult<RetryTaskDisposition, never>;
31
33
  runStartupRecovery(options?: StartupRecoveryOptions): StartupRecoveryResult;
32
34
  startScratchpadRefresh(options?: {
33
35
  lane?: ProcedureLane;
@@ -66,7 +68,9 @@ export declare class Phase5ControlPlane {
66
68
  private tryRunTaskArrivalScheduler;
67
69
  private maybeScheduleAfterRunCompletion;
68
70
  private worklistCompletionContextSyncTrigger;
69
- private maybeStartContextSyncAfterWorklistCompletion;
71
+ private scheduleContextSyncAfterWorklistCompletion;
72
+ private attemptContextSyncAfterWorklistCompletion;
73
+ private cancelPendingWorklistCompletionContextSync;
70
74
  private startTaskCompileContinuation;
71
75
  private startTaskBoundFollowUpCheck;
72
76
  private publishProcedureTransition;
@@ -1,7 +1,7 @@
1
- import { readActiveTaskCompileContext, readWorklistProjection, } from "../collaboration-state/index.js";
1
+ import { readActiveTaskCompileContext, readScratchpadSourceState, readWorklistProjection, } from "../collaboration-state/index.js";
2
2
  import { ProcedureEngine } from "../procedure-engine/index.js";
3
3
  import { RunEngine } from "../run-engine/index.js";
4
- import { WorkspaceOpsError, readReferenceSummaryTargets, } from "../workspace-ops/index.js";
4
+ import { WorkspaceOpsError, readReferenceSummaryTargets } from "../workspace-ops/index.js";
5
5
  import { publishProcedureTransition as publishProcedureTransitionEvent, publishRunResultRecorded as publishRunResultRecordedEvent, publishRunTransition as publishRunTransitionEvent, publishTaskRunStarted as publishTaskRunStartedEvent, } from "./event-publishers.js";
6
6
  import { startContextSyncAnalysisProcedure } from "./context-sync.js";
7
7
  import { refreshProjectBriefProjection, } from "./project-brief-refresh.js";
@@ -16,8 +16,10 @@ 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";
22
+ const WORKLIST_COMPLETION_CONTEXT_SYNC_GRACE_MS = 30_000;
21
23
  export class Phase5ControlPlane {
22
24
  engine;
23
25
  runEngine;
@@ -32,6 +34,7 @@ export class Phase5ControlPlane {
32
34
  runLifecycle;
33
35
  now;
34
36
  scratchpadAutoRefreshScheduler;
37
+ pendingWorklistCompletionContextSyncTimer = null;
35
38
  pendingReferenceSummaryPaths = new Set();
36
39
  referenceSummaryWaiters = new Map();
37
40
  pendingAllReferenceSummaries = false;
@@ -82,6 +85,19 @@ export class Phase5ControlPlane {
82
85
  now: this.now,
83
86
  });
84
87
  }
88
+ retryTask(taskId, payload) {
89
+ return retryControlPlaneTask({
90
+ store: this.store,
91
+ taskId,
92
+ payload,
93
+ resolveRunPipeline: this.resolveRunPipeline,
94
+ readActiveActivity: () => this.readSchedulerBlocker(),
95
+ startRunForTask: (runInput) => {
96
+ void this.startRunForTask(runInput);
97
+ },
98
+ now: this.now,
99
+ });
100
+ }
85
101
  runStartupRecovery(options = {}) {
86
102
  return runControlPlaneStartupRecovery({
87
103
  store: this.store,
@@ -98,6 +114,7 @@ export class Phase5ControlPlane {
98
114
  });
99
115
  }
100
116
  startScratchpadRefresh(options = {}) {
117
+ this.cancelPendingWorklistCompletionContextSync();
101
118
  return startControlPlaneScratchpadRefresh({
102
119
  engine: this.engine,
103
120
  store: this.store,
@@ -114,6 +131,7 @@ export class Phase5ControlPlane {
114
131
  if (!shouldUseMessageAsScratchpadSource(message)) {
115
132
  return;
116
133
  }
134
+ this.cancelPendingWorklistCompletionContextSync();
117
135
  this.scratchpadAutoRefreshScheduler.notifySourceChanged();
118
136
  }
119
137
  refreshProjectBrief(options) {
@@ -134,6 +152,7 @@ export class Phase5ControlPlane {
134
152
  }
135
153
  this.closing = true;
136
154
  this.scratchpadAutoRefreshScheduler.dispose();
155
+ this.cancelPendingWorklistCompletionContextSync();
137
156
  this.pendingAllReferenceSummaries = false;
138
157
  this.pendingReferenceSummaryPaths.clear();
139
158
  }
@@ -157,6 +176,7 @@ export class Phase5ControlPlane {
157
176
  }
158
177
  }
159
178
  startTaskCompile(payload) {
179
+ this.cancelPendingWorklistCompletionContextSync();
160
180
  const activeRun = this.readActiveRun();
161
181
  if (activeRun !== null) {
162
182
  return {
@@ -197,6 +217,9 @@ export class Phase5ControlPlane {
197
217
  });
198
218
  }
199
219
  startContextSyncAnalysis(trigger = { kind: "manual" }) {
220
+ if (trigger.kind === "manual") {
221
+ this.cancelPendingWorklistCompletionContextSync();
222
+ }
200
223
  return startContextSyncAnalysisProcedure({
201
224
  engine: this.engine,
202
225
  store: this.store,
@@ -352,6 +375,7 @@ export class Phase5ControlPlane {
352
375
  publishRunResultRecorded: (recorded) => this.publishRunResultRecorded(recorded),
353
376
  });
354
377
  if (result.kind === "accepted") {
378
+ this.cancelPendingWorklistCompletionContextSync();
355
379
  this.runLifecycle?.started();
356
380
  }
357
381
  return result;
@@ -483,7 +507,7 @@ export class Phase5ControlPlane {
483
507
  if (this.tryRunTaskArrivalScheduler()) {
484
508
  return;
485
509
  }
486
- this.maybeStartContextSyncAfterWorklistCompletion();
510
+ this.scheduleContextSyncAfterWorklistCompletion();
487
511
  }
488
512
  worklistCompletionContextSyncTrigger() {
489
513
  const worklist = readWorklistProjection(this.store.db);
@@ -497,16 +521,46 @@ export class Phase5ControlPlane {
497
521
  module_count: worklist.modules.length,
498
522
  };
499
523
  }
500
- maybeStartContextSyncAfterWorklistCompletion() {
501
- if (this.closing) {
524
+ scheduleContextSyncAfterWorklistCompletion() {
525
+ if (this.closing ||
526
+ !this.store.db.open ||
527
+ this.pendingWorklistCompletionContextSyncTimer !== null) {
528
+ return;
529
+ }
530
+ const sourceState = readScratchpadSourceState(this.store.db);
531
+ if (this.worklistCompletionContextSyncTrigger() === undefined ||
532
+ sourceState.dirty_since !== undefined ||
533
+ sourceState.in_flight !== undefined ||
534
+ this.readSchedulerBlocker() !== null) {
535
+ return;
536
+ }
537
+ this.pendingWorklistCompletionContextSyncTimer = setTimeout(() => {
538
+ this.attemptContextSyncAfterWorklistCompletion();
539
+ }, WORKLIST_COMPLETION_CONTEXT_SYNC_GRACE_MS);
540
+ this.pendingWorklistCompletionContextSyncTimer.unref?.();
541
+ }
542
+ attemptContextSyncAfterWorklistCompletion() {
543
+ this.pendingWorklistCompletionContextSyncTimer = null;
544
+ if (this.closing || !this.store.db.open) {
502
545
  return;
503
546
  }
504
547
  const trigger = this.worklistCompletionContextSyncTrigger();
505
- if (trigger === undefined || this.readSchedulerBlocker() !== null) {
548
+ const sourceState = readScratchpadSourceState(this.store.db);
549
+ if (trigger === undefined ||
550
+ sourceState.dirty_since !== undefined ||
551
+ sourceState.in_flight !== undefined ||
552
+ this.readSchedulerBlocker() !== null) {
506
553
  return;
507
554
  }
508
555
  void this.startContextSyncAnalysis(trigger);
509
556
  }
557
+ cancelPendingWorklistCompletionContextSync() {
558
+ if (this.pendingWorklistCompletionContextSyncTimer === null) {
559
+ return;
560
+ }
561
+ clearTimeout(this.pendingWorklistCompletionContextSyncTimer);
562
+ this.pendingWorklistCompletionContextSyncTimer = null;
563
+ }
510
564
  startTaskCompileContinuation(owner, roundId) {
511
565
  let tasksAdded = false;
512
566
  return startControlPlaneTaskCompileContinuation({
@@ -556,6 +610,9 @@ export class Phase5ControlPlane {
556
610
  });
557
611
  }
558
612
  publishProcedureTransition(state) {
613
+ if (state.transition_kind === "started" && state.activity.lane === "foreground") {
614
+ this.cancelPendingWorklistCompletionContextSync();
615
+ }
559
616
  this.logProcedureTransition(state);
560
617
  publishProcedureTransitionEvent({
561
618
  events: this.events,
@@ -582,6 +639,11 @@ export class Phase5ControlPlane {
582
639
  }
583
640
  if (!this.closing) {
584
641
  this.schedulePendingReferenceSummaryRefresh();
642
+ if (state.activity.status === "finished" &&
643
+ (state.activity.workflow_kind === "scratchpad_refresh" ||
644
+ state.activity.workflow_kind === "reference_summary_refresh")) {
645
+ this.scheduleContextSyncAfterWorklistCompletion();
646
+ }
585
647
  }
586
648
  }
587
649
  }
@@ -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
  ? {}