@deksden-com/dd-flow-cli 0.9.0-beta.104 → 0.9.0-beta.106

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 (69) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli/run-cli.js +23 -8
  4. package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
  5. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
  6. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
  7. package/dist/harness-runtime/lib/delegation-instructions.d.mts +3 -0
  8. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -1
  9. package/dist/protocol/local-files.js +18 -9
  10. package/dist/runtime/context.js +2 -2
  11. package/dist/services/cleanup.js +81 -32
  12. package/dist/services/code-checks.js +18 -19
  13. package/dist/services/continuation-outcome.js +1 -1
  14. package/dist/services/controller-fanout.js +12 -4
  15. package/dist/services/eval-snapshots.js +69 -41
  16. package/dist/services/execution-policy.js +12 -1
  17. package/dist/services/external-work-launch.js +2 -1
  18. package/dist/services/hooks.js +153 -43
  19. package/dist/services/lanes.js +23 -18
  20. package/dist/services/lifecycle-command.js +11 -2
  21. package/dist/services/lifecycle-invocations.js +291 -54
  22. package/dist/services/managed-processes.js +35 -23
  23. package/dist/services/merge-queue.js +71 -67
  24. package/dist/services/merge-server.js +5 -12
  25. package/dist/services/migrations.js +28 -12
  26. package/dist/services/plan-runtime.js +8 -4
  27. package/dist/services/projects.js +3 -11
  28. package/dist/services/protocols.js +98 -68
  29. package/dist/services/recovery-snapshot-database.js +43 -8
  30. package/dist/services/run-control-worker.js +15 -32
  31. package/dist/services/run-control.js +3 -17
  32. package/dist/services/run-controller-capture.js +58 -27
  33. package/dist/services/run-controller-process.js +21 -30
  34. package/dist/services/run-controller-recovery.js +36 -37
  35. package/dist/services/run-controller.js +83 -88
  36. package/dist/services/run-fork.js +10 -6
  37. package/dist/services/run-projection.js +22 -64
  38. package/dist/services/run-recovery-runtime.js +25 -14
  39. package/dist/services/run-recovery.js +56 -54
  40. package/dist/services/runs.js +218 -160
  41. package/dist/services/runtime-budget.js +19 -45
  42. package/dist/services/runtime-scope-capture.js +8 -6
  43. package/dist/services/runtime-scope-control.js +49 -39
  44. package/dist/services/runtime-scope-resume.js +134 -122
  45. package/dist/services/runtime-scope-worker.js +48 -49
  46. package/dist/services/scope-writer-cleanup.js +26 -0
  47. package/dist/services/stage-context.js +6 -1
  48. package/dist/services/stage-lifecycle.js +1 -1
  49. package/dist/services/stage-pause.js +13 -29
  50. package/dist/services/stage-report-renderer.js +116 -6
  51. package/dist/services/usage.js +2 -9
  52. package/dist/services/vnext-code-review.js +10 -7
  53. package/dist/services/vnext-code.js +9 -17
  54. package/dist/services/vnext-merge.js +64 -21
  55. package/dist/services/vnext-plan-review.js +43 -30
  56. package/dist/services/vnext-plan.js +28 -24
  57. package/dist/services/vnext-protocolize.js +183 -20
  58. package/dist/services/vnext-specify.js +27 -19
  59. package/dist/services/work-registry.js +229 -150
  60. package/dist/storage/database.js +286 -56
  61. package/dist/storage/foreign-key-transaction.js +39 -0
  62. package/dist/storage/immutable-input.js +45 -0
  63. package/dist/storage/import-guard.js +102 -0
  64. package/dist/storage/native-cleanup.js +52 -0
  65. package/dist/storage/projection-lock.js +73 -0
  66. package/dist/storage/writer-contract.js +3 -3
  67. package/dist/storage/writer-migration.js +24 -41
  68. package/package.json +1 -1
  69. package/tools/repair-paused-run-status.mjs +48 -23
@@ -14,9 +14,9 @@ const processOperation = (context, row) => `controller:${digest({ database: cont
14
14
  const birth = () => ({ pid: process.pid, pid_started_at: processStartedAt(process.pid) });
15
15
  const dead = (value) => Boolean(value && Number.isSafeInteger(value.pid) && value.pid > 0 && typeof value.pid_started_at === "string" && value.pid_started_at && !processIsAlive(value));
16
16
  /** Called inside the RUN intent transaction, before the client can disappear. */
17
- export function recordControllerLauncher(context, row, retired) {
17
+ export function recordControllerLauncher(context, row, retired, identity = birth()) {
18
18
  const request = { action: "controller_owner_launch", owner_id: launchId(row) };
19
- const receipt = { ...birth(), registration: "controller_self@1", ...(retired ? { retired_process: { id: retired.id, lease_token: retired.lease_token, pid: retired.pid, pid_started_at: retired.pid_started_at } } : {}) };
19
+ const receipt = { ...identity, registration: "controller_self@1", ...(retired ? { retired_process: { id: retired.id, lease_token: retired.lease_token, pid: retired.pid, pid_started_at: retired.pid_started_at } } : {}) };
20
20
  const now = context.now();
21
21
  context.db.run("INSERT INTO run_controller_operations (operation_id, controller_id, generation, input_hash, input_json, status, receipt_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'completed', ?, ?, ?)", [launchId(row), row.controller_id, row.generation, digest(request), JSON.stringify(request), JSON.stringify(receipt), now, now]);
22
22
  }
@@ -92,48 +92,37 @@ function prepareSpawn(context, row) {
92
92
  return null;
93
93
  if (launch.value.retired_process)
94
94
  retire(resourceContext, row, launch.value.retired_process);
95
- context.db.beginWriteTransaction();
96
- try {
95
+ const launcher = birth();
96
+ return context.db.writeTransaction(() => {
97
97
  assertLaunchAllowed(context, row);
98
98
  const changed = context.db.run(`UPDATE run_controllers SET owner_token = ?, owner_pid = NULL, process_id = NULL, process_lease_token = NULL, status = 'starting', last_error_json = NULL, updated_at = ?
99
99
  WHERE controller_id = ? AND owner_token = ? AND owner_pid IS ? AND process_id IS ? AND status = ? AND state_json = ?
100
100
  AND (SELECT receipt_json FROM run_controller_operations WHERE operation_id = ?) = ?`, [token, context.now(), row.controller_id, row.owner_token, row.owner_pid, row.process_id, row.status, row.state_json, launchId(row), launch.receipt_json]);
101
- if (changed.changes !== 1) {
102
- context.db.exec("ROLLBACK");
101
+ if (changed.changes !== 1)
103
102
  return null;
104
- }
105
103
  const current = requireController(context, row.controller_id);
106
104
  if (!ours) {
107
- recordControllerLauncher(context, current);
105
+ recordControllerLauncher(context, current, undefined, launcher);
108
106
  appendEvent(context, row.controller_id, "launch_owner_reconciled", { prior_launch: launchId(row), generation: row.generation, prior_process_id: row.process_id });
109
107
  }
110
108
  context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(receipt_json, '$.spawn_started', json('true')), updated_at = ? WHERE operation_id = ?", [context.now(), launchId(current)]);
111
- context.db.exec("COMMIT");
112
109
  return current;
113
- }
114
- catch (error) {
115
- context.db.exec("ROLLBACK");
116
- throw error;
117
- }
110
+ });
118
111
  }
119
112
  /** The physical child records its own birth before resource registration. */
120
113
  export function claimControllerProcess(context, row) {
121
114
  const launch = launchRecord(context, row);
122
115
  if (!launch && (!row.process_id || !row.process_lease_token))
123
116
  throw new AppError("controller_owner_mismatch", "Managed registration is not authorized", 1);
124
- context.db.beginWriteTransaction();
125
- try {
117
+ const owner = birth();
118
+ context.db.writeTransaction(() => {
126
119
  const claimed = context.db.run("UPDATE run_controllers SET owner_pid = ?, status = 'running', updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [process.pid, context.now(), row.controller_id, row.owner_token]);
127
120
  if (claimed.changes !== 1)
128
121
  throw new AppError("controller_already_owned", "Managed owner has already claimed this launch", 1);
129
122
  if (launch)
130
- context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(receipt_json, '$.owner', json(?)), updated_at = ? WHERE operation_id = ?", [JSON.stringify(birth()), context.now(), launchId(row)]);
131
- context.db.exec("COMMIT");
132
- }
133
- catch (error) {
134
- context.db.exec("ROLLBACK");
135
- throw error;
136
- }
123
+ context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(receipt_json, '$.owner', json(?)), updated_at = ? WHERE operation_id = ?", [JSON.stringify(owner), context.now(), launchId(row)]);
124
+ });
125
+ row.owner_pid = process.pid;
137
126
  if (!row.process_id) {
138
127
  const manifest = JSON.parse(row.manifest_json);
139
128
  const resourceContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: manifest.resource_home } };
@@ -173,16 +162,18 @@ export async function spawnController(context, row) {
173
162
  return controllerReceipt(requireController(context, controllerId));
174
163
  }
175
164
  catch (error) {
176
- context.db.exec("SAVEPOINT controller_spawn_status");
177
165
  try {
178
- const changed = context.db.run("UPDATE run_controllers SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [JSON.stringify(errorRecord(error)), context.now(), controllerId, token]);
179
- if (changed.changes && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation)
180
- setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
181
- context.db.exec("RELEASE controller_spawn_status");
166
+ context.db.writeTransaction(() => {
167
+ const changed = context.db.run("UPDATE run_controllers SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [JSON.stringify(errorRecord(error)), context.now(), controllerId, token]);
168
+ if (changed.changes && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation)
169
+ setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
170
+ });
182
171
  }
183
172
  catch (failure) {
184
- context.db.exec("ROLLBACK TO controller_spawn_status; RELEASE controller_spawn_status");
185
- throw failure;
173
+ const secondary = { status_persistence_error: errorRecord(failure) };
174
+ if (error instanceof AppError)
175
+ throw new AppError(error.code, error.message, error.exitCode, { ...error.details, ...secondary });
176
+ throw new AppError("controller_spawn_failed", error instanceof Error ? error.message : String(error), 1, { cause: errorRecord(error), ...secondary });
186
177
  }
187
178
  throw error;
188
179
  }
@@ -10,7 +10,7 @@ import { verifyRecoverySnapshot } from "./eval-snapshots.js";
10
10
  import { recoveryGuard, resumeRunRecovery } from "./run-recovery.js";
11
11
  import { prepareRunRecoveryDispatch } from "./run-recovery-runtime.js";
12
12
  import { runtimeHarness } from "./execution-policy.js";
13
- import { managedProcessRuntimeHome, managedProcessStatus, processIsAlive, processTreeIsAlive } from "./managed-processes.js";
13
+ import { managedProcessBinding, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, processTreeIsAlive, processStartedAt } from "./managed-processes.js";
14
14
  import { requireRun, requireController, controllerReceipt, appendEvent, saveState, digest, errorRecord } from "./run-controller-state.js";
15
15
  import { controllerAdapter, sessionArguments } from "./run-controller-adapter.js";
16
16
  import { spawnController, recordControllerLauncher } from "./run-controller-process.js";
@@ -18,6 +18,7 @@ import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-da
18
18
  import { managedInvocationContext } from "./lifecycle-invocations.js";
19
19
  import { runControlStatus } from "./run-control.js";
20
20
  import { runtimeBudgetStatus, waitForRuntimeScopeRecovery } from "./runtime-budget.js";
21
+ import { prepareControllerPublication } from "./run-controller-capture.js";
21
22
  /** CLI observation is bounded; the physical controller owns the recovery work. */
22
23
  export async function resumeRunController(context, input) {
23
24
  if (input.waitMs !== undefined && (!Number.isSafeInteger(input.waitMs) || input.waitMs < 0 || input.waitMs > 60_000))
@@ -35,6 +36,11 @@ export async function resumeRunController(context, input) {
35
36
  }
36
37
  /** Retain the logical controller and its receipts; replace only its dead owner. */
37
38
  export function recordControllerResume(context, input) {
39
+ return prepareControllerResume(context, input)();
40
+ }
41
+ /** Verify immutable recovery evidence before a caller reserves the registry
42
+ * writer. The returned operation rechecks SQL ownership under the RUN CAS. */
43
+ export function prepareControllerResume(context, input) {
38
44
  if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !input.controlId.trim())
39
45
  throw new AppError("usage", "Resume requires request-id and the exact source control", 2);
40
46
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -53,7 +59,7 @@ export function recordControllerResume(context, input) {
53
59
  };
54
60
  const prior = findPrior();
55
61
  if (prior)
56
- return prior;
62
+ return () => findPrior() ?? prior;
57
63
  try {
58
64
  const row = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [project.id, run.id]);
59
65
  if (!row)
@@ -78,18 +84,20 @@ export function recordControllerResume(context, input) {
78
84
  if (!previousOwner || previousOwner.lease_token !== row.process_lease_token || previousOwner.kind !== "run-controller" || previousOwner.owner_id !== row.controller_id || previousOwner.project_id !== project.id || previousOwner.run_id !== run.id || managedProcessRuntimeHome(previousOwner, context) !== context.ddFlowHome || !previousOwner.pid || !previousOwner.pid_started_at || processTreeIsAlive(previousOwner))
79
85
  throw new AppError("controller_owner_unsettled", "Resume requires proof that the previous physical controller has exited", 1);
80
86
  const token = crypto.randomUUID(), now = context.now();
81
- context.db.beginWriteTransaction();
82
- try {
87
+ const launcher = { pid: process.pid, pid_started_at: processStartedAt(process.pid) };
88
+ return () => context.db.writeTransaction(() => {
83
89
  const duplicate = context.db.get("SELECT input_hash, status FROM run_controller_operations WHERE operation_id = ?", [operationId]);
84
90
  if (duplicate) {
85
91
  if (duplicate.input_hash !== hash)
86
92
  throw new AppError("runtime_request_conflict", "Resume request id has different inputs", 1);
87
- context.db.exec("COMMIT");
88
93
  return { controller: requireController(context, row.controller_id), operationId };
89
94
  }
90
95
  const current = recoveryGuard(context, project.id, run.id);
91
96
  if (current?.recovery_id !== guard.recovery_id || current.generation !== guard.generation || current.status !== "sealed" || current.capture_path !== guard.capture_path)
92
97
  throw new AppError("run_control_superseded", "Control changed before resume could be accepted", 1);
98
+ const currentOwner = managedProcessStatus(resourceContext).find(record => record.id === previousOwner.id);
99
+ if (!currentOwner || digest(managedProcessBinding(currentOwner, manifest.resource_home)) !== digest(managedProcessBinding(previousOwner, manifest.resource_home)))
100
+ throw new AppError("controller_owner_unsettled", "Retired controller binding changed while recovery was prepared", 1);
93
101
  if (!["running", "paused", "waiting_for_user"].includes(requireRun(context, project.id, run.id).status))
94
102
  throw new AppError("invalid_run_state", "RUN became terminal before resume could be accepted", 1);
95
103
  const changed = context.db.run("UPDATE run_controls SET status = 'resuming', updated_at = ? WHERE control_id = ? AND recovery_id = ? AND status = 'settled'", [now, input.controlId, guard.recovery_id]);
@@ -100,22 +108,17 @@ export function recordControllerResume(context, input) {
100
108
  if (claimed.changes !== 1)
101
109
  throw new AppError("controller_owner_mismatch", "Controller changed before resume could claim its next physical owner", 1);
102
110
  context.db.run("INSERT INTO run_controller_operations (operation_id, controller_id, generation, input_hash, input_json, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'accepted', ?, ?)", [operationId, row.controller_id, guard.generation, hash, JSON.stringify(request), now, now]);
103
- recordControllerLauncher(context, requireController(context, row.controller_id), previousOwner);
111
+ recordControllerLauncher(context, requireController(context, row.controller_id), previousOwner, launcher);
104
112
  appendEvent(context, row.controller_id, "resume_requested", { operation_id: operationId, control_id: input.controlId, recovery_id: guard.recovery_id, generation: guard.generation, retired_process_id: previousOwner.id, retained_session_id: state.sessions[state.current_session].id });
105
- context.db.exec("COMMIT");
106
- }
107
- catch (error) {
108
- context.db.exec("ROLLBACK");
109
- throw error;
110
- }
111
- return { controller: requireController(context, row.controller_id), operationId, retiredProcess: previousOwner };
113
+ return { controller: requireController(context, row.controller_id), operationId, retiredProcess: previousOwner };
114
+ });
112
115
  }
113
116
  catch (error) {
114
117
  // A concurrent identical client may commit between our first lookup and
115
118
  // validation. Its durable request wins even if our preflight saw the fence.
116
119
  const concurrent = findPrior();
117
120
  if (concurrent)
118
- return concurrent;
121
+ return () => concurrent;
119
122
  throw error;
120
123
  }
121
124
  }
@@ -126,8 +129,9 @@ export async function resumeControllerSession(context, row, manifest, state, ass
126
129
  throw new AppError("recovery_owner_missing", "Recovery lost its retained coordinator Session", 1);
127
130
  let executionContext = { ...context, env: { ...context.env, DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home } };
128
131
  const scopeControl = manifest.budget ? runtimeBudgetStatus(executionContext, manifest.budget.scope_id).control : null;
129
- const assertRecoveryOwner = () => {
130
- assertPhysicalOwner();
132
+ const assertRecoveryOwner = (physical = true) => {
133
+ if (physical)
134
+ assertPhysicalOwner();
131
135
  const guard = recoveryGuard(context, row.project_id, row.run_id);
132
136
  if (guard?.generation !== row.generation || guard.recovery_id !== recovery.recovery_id)
133
137
  throw new AppError("run_recovery_generation_stale", "A newer control superseded this recovery owner", 1);
@@ -168,22 +172,21 @@ export async function resumeControllerSession(context, row, manifest, state, ass
168
172
  throw new AppError("recovery_session_unsettled", "Retained native Session is not ready for same-Session recovery", 1);
169
173
  assertRecoveryOwner();
170
174
  const prepared = { prepared: true, recovery_id: recovery.recovery_id, session_id: session.id, daemon_id: daemonId, process_id: dispatch.process_id, resume_operation_id: dispatch.resume_operation_id };
171
- context.db.beginWriteTransaction();
172
- try {
173
- assertRecoveryOwner();
174
- const binding = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id, ownerId: session.owner.owner_id });
175
- if (binding.process.id !== dispatch.process_id || binding.state.daemon_id !== daemonId || binding.process.state !== "running" || !processIsAlive(binding.process))
176
- throw new AppError("recovery_runtime_mismatch", "Prepared recovery lost its physical Session owner", 1);
175
+ const binding = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id, ownerId: session.owner.owner_id });
176
+ if (binding.process.id !== dispatch.process_id || binding.state.daemon_id !== daemonId || binding.process.state !== "running" || !processIsAlive(binding.process))
177
+ throw new AppError("recovery_runtime_mismatch", "Prepared recovery lost its physical Session owner", 1);
178
+ const assertPreparedOwner = prepareControllerPublication(executionContext, row, assertPhysicalOwner);
179
+ context.db.writeTransaction(() => {
180
+ assertPreparedOwner();
181
+ assertRecoveryOwner(false);
182
+ const currentNative = managedProcessStatus(executionContext).find(item => item.id === binding.process.id);
183
+ if (!currentNative || currentNative.state !== "running" || currentNative.lease_expires_at <= context.now() || digest(managedProcessBinding(currentNative, manifest.resource_home)) !== digest(managedProcessBinding(binding.process, manifest.resource_home)))
184
+ throw new AppError("recovery_runtime_mismatch", "Prepared recovery lost its current native lease", 1);
177
185
  const recorded = context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'accepted'", [JSON.stringify(prepared), context.now(), recovery.operation_id, row.controller_id, row.generation]);
178
186
  if (recorded.changes !== 1)
179
187
  throw new AppError("run_recovery_generation_stale", "Recovery preparation no longer owns its operation", 1);
180
188
  appendEvent(context, row.controller_id, "recovery_prepared", prepared);
181
- context.db.exec("COMMIT");
182
- }
183
- catch (error) {
184
- context.db.exec("ROLLBACK");
185
- throw error;
186
- }
189
+ });
187
190
  if (scopeControl && manifest.budget) {
188
191
  await waitForRuntimeScopeRecovery(executionContext, { scopeId: manifest.budget.scope_id, generation: scopeControl.generation, controllerId: row.controller_id, operationId: recovery.operation_id }, assertRecoveryOwner);
189
192
  }
@@ -195,9 +198,10 @@ export async function resumeControllerSession(context, row, manifest, state, ass
195
198
  assertRecoveryOwner();
196
199
  if (recoveryGuard(context, row.project_id, row.run_id)?.status !== "recovered")
197
200
  throw new AppError("trusted_recovery_acceptance_required", "Recovery Turn ended without its native acknowledgement", 1);
198
- context.db.beginWriteTransaction();
199
- try {
200
- assertRecoveryOwner();
201
+ const assertAcceptedOwner = prepareControllerPublication(executionContext, row, assertPhysicalOwner);
202
+ context.db.writeTransaction(() => {
203
+ assertAcceptedOwner();
204
+ assertRecoveryOwner(false);
201
205
  context.db.run("UPDATE run_controls SET status = 'resumed', updated_at = ? WHERE control_id = ? AND status = 'resuming'", [context.now(), recovery.control_id]);
202
206
  context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'accepted'", [JSON.stringify({ resumed: true, recovery_id: recovery.recovery_id, session_id: session.id, daemon_id: daemonId }), context.now(), recovery.operation_id]);
203
207
  state.last_receipt = receipt;
@@ -205,12 +209,7 @@ export async function resumeControllerSession(context, row, manifest, state, ass
205
209
  delete state.recovery;
206
210
  saveState(context, row, state);
207
211
  appendEvent(context, row.controller_id, "recovery_resumed", { control_id: recovery.control_id, recovery_id: recovery.recovery_id, session_id: session.id, daemon_id: daemonId, generation: row.generation });
208
- context.db.exec("COMMIT");
209
- }
210
- catch (error) {
211
- context.db.exec("ROLLBACK");
212
- throw error;
213
- }
212
+ });
214
213
  }
215
214
  export function failControllerRecovery(context, row, state, error) {
216
215
  if (state.recovery) {