@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +526 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +89 -13
- package/dist/cli/hook-ingress.js +77 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +48 -0
- package/dist/cli/run-cli.js +1465 -384
- package/dist/cli.js +8 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +394 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +71 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/code-work-result.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +132 -39
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +589 -106
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +173 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +994 -97
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +166 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +702 -327
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +113 -18
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1379 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +302 -34
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +66 -28
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +25 -16
- package/dist/services/protocols.js +160 -86
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +142 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +351 -0
- package/dist/services/run-control.js +888 -0
- package/dist/services/run-controller-adapter.js +231 -0
- package/dist/services/run-controller-capture.js +167 -0
- package/dist/services/run-controller-process.js +180 -0
- package/dist/services/run-controller-recovery.js +221 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +798 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +142 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +80 -0
- package/dist/services/run-recovery.js +345 -0
- package/dist/services/runs.js +536 -181
- package/dist/services/runtime-budget.js +413 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +460 -0
- package/dist/services/runtime-scope-resume.js +577 -0
- package/dist/services/runtime-scope-stop.js +159 -0
- package/dist/services/runtime-scope-worker.js +242 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +50 -19
- package/dist/services/stage-lifecycle.js +100 -103
- package/dist/services/stage-pause.js +88 -75
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +35 -98
- package/dist/services/vnext-code-review.js +197 -84
- package/dist/services/vnext-code.js +278 -251
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +172 -17
- package/dist/services/vnext-merge.js +345 -111
- package/dist/services/vnext-plan-review.js +171 -111
- package/dist/services/vnext-plan.js +180 -87
- package/dist/services/vnext-protocolize.js +320 -74
- package/dist/services/vnext-specify.js +115 -77
- package/dist/services/work-registry.js +1001 -187
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +88 -36
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +692 -35
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +24 -13
- package/tools/repair-paused-run-status.mjs +84 -0
package/dist/services/prompts.js
CHANGED
|
@@ -9,7 +9,8 @@ import { validatePlanItem } from "../domain/validation.js";
|
|
|
9
9
|
import { AppError } from "../shared/errors.js";
|
|
10
10
|
import { parseJsonObject } from "../shared/json.js";
|
|
11
11
|
import { ensureDir, resolveProjectRoot } from "../storage/paths.js";
|
|
12
|
-
import { planWithProgress } from "./plan-runtime.js";
|
|
12
|
+
import { boundCanonicalPlan, planWithProgress, prepareCanonicalPlan } from "./plan-runtime.js";
|
|
13
|
+
import { prepareTextFile } from "../cli/input-preparation.js";
|
|
13
14
|
import { registerFlowJob } from "./sessions.js";
|
|
14
15
|
const profiles = {
|
|
15
16
|
code_implementation: {
|
|
@@ -22,7 +23,7 @@ const profiles = {
|
|
|
22
23
|
static_files: [".memory-bank/dd-flow/common/worker-session.md", ".memory-bank/dd-flow/workers/verify.md"]
|
|
23
24
|
}
|
|
24
25
|
};
|
|
25
|
-
export function
|
|
26
|
+
export function prepareWorkerPrompt(context, input) {
|
|
26
27
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
27
28
|
const project = requireProjectByRoot(context, projectRoot);
|
|
28
29
|
const protocol = requireProtocol(context, protocolIdForRun(context, project.id, input.runId), project.id);
|
|
@@ -43,8 +44,10 @@ export function renderWorkerPrompt(context, input) {
|
|
|
43
44
|
projectId: protocol.project_id,
|
|
44
45
|
protocolId: protocol.id,
|
|
45
46
|
planPath: protocol.plan_path
|
|
46
|
-
});
|
|
47
|
+
}, { bind: false });
|
|
47
48
|
const plan = planRuntime?.plan ?? null;
|
|
49
|
+
if (planRuntime)
|
|
50
|
+
prepareCanonicalPlan(context, { projectId: protocol.project_id, protocolId: protocol.id, planPath: protocol.plan_path }, planRuntime.canonical);
|
|
48
51
|
const genericTask = input.taskFile ? readWorkerTask(input.taskFile, runHomePath(run)) : null;
|
|
49
52
|
const item = genericTask?.item ?? plan?.items.find((candidate) => candidate.id === input.planItemId);
|
|
50
53
|
if (!item)
|
|
@@ -64,13 +67,6 @@ export function renderWorkerPrompt(context, input) {
|
|
|
64
67
|
const profile = profiles[input.profile];
|
|
65
68
|
if (!profile)
|
|
66
69
|
throw new AppError("prompt_profile_unsupported", `Unsupported prompt profile: ${input.profile}`, 2);
|
|
67
|
-
const job = registerFlowJob(context, {
|
|
68
|
-
projectId: protocol.project_id,
|
|
69
|
-
runId: run.id,
|
|
70
|
-
protocolId: protocol.id,
|
|
71
|
-
planItemId: item.id,
|
|
72
|
-
groupId: item.execution_context.planned_write_areas[0] ?? null
|
|
73
|
-
});
|
|
74
70
|
const staticInputs = profile.static_files.map((file) => readStaticInput(projectRoot, file));
|
|
75
71
|
const requiredRead = item.execution_context.required_read.map((file) => checkedReference(projectRoot, runHomePath(run), file, "required_read", true));
|
|
76
72
|
const discoveryBoundary = item.execution_context.discovery_boundary.map((file) => checkedReference(projectRoot, runHomePath(run), file, "discovery_boundary", false));
|
|
@@ -78,7 +74,6 @@ export function renderWorkerPrompt(context, input) {
|
|
|
78
74
|
const runHome = runHomePath(run);
|
|
79
75
|
const outputDir = path.join(runHome, stage.dir, "subagents", item.id);
|
|
80
76
|
assertWithin(runHome, outputDir, "output directory");
|
|
81
|
-
ensureDir(outputDir);
|
|
82
77
|
const prompt = renderPrompt({
|
|
83
78
|
item,
|
|
84
79
|
input,
|
|
@@ -90,6 +85,20 @@ export function renderWorkerPrompt(context, input) {
|
|
|
90
85
|
staticInputs,
|
|
91
86
|
...(genericTask ? { handoff: genericTask.handoff } : {})
|
|
92
87
|
});
|
|
88
|
+
return { protocol, run, item, plan, planRuntime, genericTask, workspaceRoot, staticInputs, requiredRead, discoveryBoundary, plannedWriteAreas, outputDir, prompt, canonVersion: readCanonVersion(projectRoot) };
|
|
89
|
+
}
|
|
90
|
+
export function renderWorkerPrompt(context, input, prepared = prepareWorkerPrompt(context, input)) {
|
|
91
|
+
const { protocol, run, item, plan, planRuntime, genericTask, workspaceRoot, staticInputs, requiredRead, discoveryBoundary, plannedWriteAreas, outputDir, prompt, canonVersion } = prepared;
|
|
92
|
+
if (planRuntime)
|
|
93
|
+
boundCanonicalPlan(context, { projectId: protocol.project_id, protocolId: protocol.id, planPath: protocol.plan_path }, planRuntime.canonical);
|
|
94
|
+
const job = registerFlowJob(context, {
|
|
95
|
+
projectId: protocol.project_id,
|
|
96
|
+
runId: run.id,
|
|
97
|
+
protocolId: protocol.id,
|
|
98
|
+
planItemId: item.id,
|
|
99
|
+
groupId: item.execution_context.planned_write_areas[0] ?? null
|
|
100
|
+
});
|
|
101
|
+
ensureDir(outputDir);
|
|
93
102
|
const promptPath = path.join(outputDir, "launch-prompt.md");
|
|
94
103
|
const stackPath = path.join(outputDir, "prompt-stack.json");
|
|
95
104
|
const reportPath = path.join(outputDir, "render-report.json");
|
|
@@ -104,7 +113,7 @@ export function renderWorkerPrompt(context, input) {
|
|
|
104
113
|
plan_item_id: input.taskFile ? null : item.id,
|
|
105
114
|
task_id: input.taskFile ? item.id : null,
|
|
106
115
|
job_id: job.job_id,
|
|
107
|
-
canon_version:
|
|
116
|
+
canon_version: canonVersion,
|
|
108
117
|
renderer_version: getCliVersionReport().cli.version,
|
|
109
118
|
static_inputs: staticInputs.map(({ path: inputPath, sha256 }) => ({ path: inputPath, sha256 })),
|
|
110
119
|
validation: { required_read: requiredRead, discovery_boundary: discoveryBoundary, planned_write_areas: plannedWriteAreas },
|
|
@@ -143,9 +152,7 @@ function runHomePath(run) {
|
|
|
143
152
|
function readWorkerTask(taskFile, runHome) {
|
|
144
153
|
const resolved = path.resolve(taskFile);
|
|
145
154
|
assertWithin(runHome, resolved, "worker task file");
|
|
146
|
-
|
|
147
|
-
throw new AppError("not_found", "Worker task file is missing", 1, { path: taskFile });
|
|
148
|
-
const task = parseJsonObject(fs.readFileSync(resolved, "utf8"), resolved);
|
|
155
|
+
const task = parseJsonObject(prepareTextFile(resolved, "worker task").text, resolved);
|
|
149
156
|
if (task.schema_id !== "dd-flow/worker-task@1" || !task.task || typeof task.task !== "object") {
|
|
150
157
|
throw new AppError("validation", "Worker task must use dd-flow/worker-task@1 with a task object", 2, { path: taskFile });
|
|
151
158
|
}
|
|
@@ -180,7 +187,7 @@ function validateWorkerTaskHandoff(value, runHome) {
|
|
|
180
187
|
const reportPath = path.resolve(report.report_path);
|
|
181
188
|
assertWithin(runHome, reportPath, "predecessor report");
|
|
182
189
|
if (!fs.existsSync(reportPath))
|
|
183
|
-
throw new AppError("
|
|
190
|
+
throw new AppError("runtime_artifact_missing", "Accepted predecessor report is missing", 1, { path: report.report_path });
|
|
184
191
|
return { aspect_id: report.aspect_id, verdict, report_path: reportPath };
|
|
185
192
|
});
|
|
186
193
|
const recovery = handoff.recovery_attempt_paths ?? [];
|
|
@@ -293,6 +300,8 @@ function renderPrompt(input) {
|
|
|
293
300
|
`- Acceptance contribution: ${item.semantic_spine?.acceptance_contribution ?? "not_applicable"}`,
|
|
294
301
|
"",
|
|
295
302
|
"## Hard Write Boundary",
|
|
303
|
+
"Before editing, change into the project workspace shown below (cd -- WORKSPACE) or use its absolute path when the tool cannot persist cwd.",
|
|
304
|
+
"Write only the project workspace and the named report directory; never write another RUN/native home or Git/worktree control data.",
|
|
296
305
|
`- All project writes must stay under: \`${input.workspaceRoot}\``,
|
|
297
306
|
"- Do not write outside this workspace, to another RUN, or into Git/worktree control data.",
|
|
298
307
|
"- Accepted requirements and non-goals remain binding; planned write areas do not.",
|
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
3
4
|
import { spawnSync } from "node:child_process";
|
|
4
5
|
import { parse as parseYaml } from "yaml";
|
|
5
6
|
import { canTransition } from "../domain/contracts.js";
|
|
6
7
|
import { defaultFlowContract, flowContractForState, loadProjectFlowContract } from "../domain/flow-contract.js";
|
|
7
8
|
import { requireStage } from "../domain/validation.js";
|
|
8
9
|
import { AppError } from "../shared/errors.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { ensureDir, planJsonPath, resolveProjectRoot, runtimeProtocolDir, runtimeStateJsonPath } from "../storage/paths.js";
|
|
10
|
+
import { prepareJsonFile } from "../cli/input-preparation.js";
|
|
11
|
+
import { ensureDir, planJsonPath, resolveProjectRoot, resolveProtocolPlanPath, runtimeProtocolDir, runtimeStateJsonPath } from "../storage/paths.js";
|
|
12
12
|
import { appendAudit, getAuditEvents } from "./audit.js";
|
|
13
|
+
import { withProjectionLock } from "../storage/projection-lock.js";
|
|
13
14
|
import { requireProjectByRoot } from "./projects.js";
|
|
14
|
-
import {
|
|
15
|
+
import { prepareRuntimeProtocolFiles, publishRuntimeProtocolMetadata, readStateFile, writeState } from "../protocol/local-files.js";
|
|
15
16
|
import { planSummary, planWithProgress, readCanonicalPlan } from "./plan-runtime.js";
|
|
16
17
|
import { activeCodexSessionBindingsForProject, activeFlowSessionBindingsForProject, codexHomeProfilesForProject, codexHookEventsForProject, hookStatusForProject } from "./hooks.js";
|
|
17
18
|
import { buildProtocolFlowGuidance } from "./flow-guidance.js";
|
|
18
19
|
import { lifecycleIsTerminalFailure, lifecycleIsTerminalSuccess, normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
19
20
|
import { getProtocolBranchContext } from "./branch-context.js";
|
|
20
|
-
export function
|
|
21
|
+
export function prepareProtocolRegistration(context, input) {
|
|
22
|
+
if (typeof input.handshakeId !== "string" || !input.handshakeId.trim() || /[/\\\0]/.test(input.handshakeId) || [".", ".."].includes(input.handshakeId))
|
|
23
|
+
throw new AppError("validation", "Protocol handshake must be a non-empty identifier, not a path", 2);
|
|
21
24
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
22
25
|
const project = requireProjectByRoot(context, projectRoot);
|
|
23
|
-
const now = context.now();
|
|
24
26
|
const workspacePath = input.workspacePath ? resolveProjectRoot(input.workspacePath) : undefined;
|
|
25
27
|
const idRoot = workspacePath ?? projectRoot;
|
|
26
28
|
const protocolId = inferProtocolId(idRoot, input.handshakeId);
|
|
27
29
|
const runtimeDir = runtimeProtocolDir(context.ddFlowHome, project.id, protocolId);
|
|
28
|
-
|
|
30
|
+
return { projectRoot, project, workspacePath, protocolId, runtimeDir };
|
|
31
|
+
}
|
|
32
|
+
export function registerProtocol(context, input) {
|
|
33
|
+
const { projectRoot, project, workspacePath, protocolId, runtimeDir } = prepareProtocolRegistration(context, input);
|
|
34
|
+
const now = context.now();
|
|
35
|
+
const initialized = prepareRuntimeProtocolFiles({
|
|
29
36
|
runtimeDir,
|
|
30
37
|
protocolId,
|
|
31
38
|
projectRoot,
|
|
@@ -33,25 +40,27 @@ export function registerProtocol(context, input) {
|
|
|
33
40
|
workspacePath,
|
|
34
41
|
now
|
|
35
42
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
protocolId
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
return context.db.writeTransaction(() => {
|
|
44
|
+
const existing = findProtocol(context, protocolId, project.id);
|
|
45
|
+
const state = existing ? latestProtocolProjection(context, project.id, protocolId) ?? initialized : initialized;
|
|
46
|
+
const params = [
|
|
47
|
+
protocolId,
|
|
48
|
+
input.handshakeId,
|
|
49
|
+
project.id,
|
|
50
|
+
projectRoot,
|
|
51
|
+
state.status,
|
|
52
|
+
state.stage,
|
|
53
|
+
state.next_action,
|
|
54
|
+
JSON.stringify(state.route),
|
|
55
|
+
JSON.stringify(state.workspace),
|
|
56
|
+
JSON.stringify(state.blockers),
|
|
57
|
+
JSON.stringify(state.active_def),
|
|
58
|
+
runtimeStateJsonPath(context.ddFlowHome, project.id, protocolId),
|
|
59
|
+
planJsonPath(workspacePath ?? projectRoot, protocolId),
|
|
60
|
+
existing?.created_at ?? now,
|
|
61
|
+
now
|
|
62
|
+
];
|
|
63
|
+
context.db.run(`INSERT INTO protocols
|
|
55
64
|
(id, handshake_id, project_id, project_root, status, stage, next_action,
|
|
56
65
|
route_json, workspace_json, blockers_json, active_def_json, state_path, plan_path,
|
|
57
66
|
created_at, updated_at)
|
|
@@ -70,19 +79,23 @@ export function registerProtocol(context, input) {
|
|
|
70
79
|
state_path = excluded.state_path,
|
|
71
80
|
plan_path = excluded.plan_path,
|
|
72
81
|
updated_at = excluded.updated_at`, params);
|
|
73
|
-
|
|
74
|
-
protocolId
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
appendAudit(context, { protocolId, projectId: project.id, eventType: "protocol.state_projection", payload: { state } });
|
|
83
|
+
context.db.afterCommit(() => publishProtocolProjection(context, project.id, protocolId));
|
|
84
|
+
context.db.afterCommit(() => publishRuntimeProtocolMetadata({ runtimeDir, protocolId, handshakeId: input.handshakeId, projectRoot }));
|
|
85
|
+
appendAudit(context, {
|
|
86
|
+
protocolId,
|
|
87
|
+
projectId: project.id,
|
|
88
|
+
eventType: existing ? "protocol.registration_refreshed" : "protocol.registered",
|
|
89
|
+
payload: {
|
|
90
|
+
protocol_id: protocolId,
|
|
91
|
+
handshake_id: input.handshakeId,
|
|
92
|
+
project_root: projectRoot,
|
|
93
|
+
workspace_path: workspacePath ?? null,
|
|
94
|
+
runtime_dir: runtimeDir
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return { ok: true, protocol: protocolStatusPayload(requireProtocol(context, protocolId, project.id)), state };
|
|
98
|
+
}, "protocol.register");
|
|
86
99
|
}
|
|
87
100
|
export function getProtocolStatus(context, input) {
|
|
88
101
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -243,7 +256,22 @@ export function implementProtocol(context, input) {
|
|
|
243
256
|
}
|
|
244
257
|
};
|
|
245
258
|
}
|
|
246
|
-
export function
|
|
259
|
+
export function prepareProtocolTransition(context, input) {
|
|
260
|
+
const { value } = prepareJsonFile(input.jsonFile, "Protocol transition payload");
|
|
261
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
262
|
+
throw new AppError("validation", "Protocol transition payload must be an object", 2);
|
|
263
|
+
const payload = value;
|
|
264
|
+
for (const [key, field] of Object.entries(payload)) {
|
|
265
|
+
const valid = key === "next_action" ? typeof field === "string"
|
|
266
|
+
: ["route", "workspace"].includes(key) ? Boolean(field && typeof field === "object" && !Array.isArray(field))
|
|
267
|
+
: ["blockers", "active_def"].includes(key) && Array.isArray(field);
|
|
268
|
+
if (!valid)
|
|
269
|
+
throw new AppError("validation", `Unsupported field or invalid value in protocol transition: ${key}`, 2, { file: input.jsonFile, parameter: key });
|
|
270
|
+
}
|
|
271
|
+
protocolTransitionState(context, input);
|
|
272
|
+
return payload;
|
|
273
|
+
}
|
|
274
|
+
function protocolTransitionState(context, input) {
|
|
247
275
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
248
276
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
249
277
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
@@ -256,8 +284,10 @@ export function transitionProtocol(context, input) {
|
|
|
256
284
|
if (input.force && (!input.reason || input.reason.trim().length === 0)) {
|
|
257
285
|
throw new AppError("validation", "Forced transitions require --reason", 2);
|
|
258
286
|
}
|
|
259
|
-
|
|
260
|
-
|
|
287
|
+
return { project, protocol, state, flowContract, from, to };
|
|
288
|
+
}
|
|
289
|
+
export function transitionProtocol(context, input, payload = prepareProtocolTransition(context, input)) {
|
|
290
|
+
const { project, protocol, state, flowContract, from, to } = protocolTransitionState(context, input);
|
|
261
291
|
const nextState = {
|
|
262
292
|
...state,
|
|
263
293
|
stage: to,
|
|
@@ -416,8 +446,7 @@ export function syncProtocolFromRun(context, input) {
|
|
|
416
446
|
next_action: nextActionForSyncedTarget(targetStage),
|
|
417
447
|
updated_at: now
|
|
418
448
|
};
|
|
419
|
-
context.db.
|
|
420
|
-
try {
|
|
449
|
+
context.db.writeTransaction(() => {
|
|
421
450
|
persistProtocolState(context, protocol, nextState);
|
|
422
451
|
if (targetStage === "ready_for_merge") {
|
|
423
452
|
context.db.run(`INSERT INTO merge_queue
|
|
@@ -449,12 +478,7 @@ export function syncProtocolFromRun(context, input) {
|
|
|
449
478
|
stage_chain: stageChain(index)
|
|
450
479
|
}
|
|
451
480
|
});
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
catch (error) {
|
|
455
|
-
context.db.exec("ROLLBACK");
|
|
456
|
-
throw error;
|
|
457
|
-
}
|
|
481
|
+
}, "protocols.syncFromRun");
|
|
458
482
|
return {
|
|
459
483
|
ok: true,
|
|
460
484
|
protocol_id: protocol.id,
|
|
@@ -486,8 +510,7 @@ export function cancelProtocol(context, input) {
|
|
|
486
510
|
const now = context.now();
|
|
487
511
|
const worktree = context.db.get("SELECT worktree_path, status FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
488
512
|
const worktreeRemoval = input.worktree === "remove" ? removeProtocolWorktreeBeforeCancel(protocol, state, worktree, input.force) : undefined;
|
|
489
|
-
context.db.
|
|
490
|
-
try {
|
|
513
|
+
return context.db.writeTransaction(() => {
|
|
491
514
|
const nextState = {
|
|
492
515
|
...state,
|
|
493
516
|
stage: "cancelled",
|
|
@@ -548,7 +571,6 @@ export function cancelProtocol(context, input) {
|
|
|
548
571
|
worktree_outcome: worktreeOutcome
|
|
549
572
|
}
|
|
550
573
|
});
|
|
551
|
-
context.db.exec("COMMIT");
|
|
552
574
|
return {
|
|
553
575
|
ok: true,
|
|
554
576
|
protocol_id: protocol.id,
|
|
@@ -560,11 +582,7 @@ export function cancelProtocol(context, input) {
|
|
|
560
582
|
worktree_outcome: worktreeOutcome,
|
|
561
583
|
worktree: context.db.get("SELECT * FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]) ?? null
|
|
562
584
|
};
|
|
563
|
-
}
|
|
564
|
-
catch (error) {
|
|
565
|
-
context.db.exec("ROLLBACK");
|
|
566
|
-
throw error;
|
|
567
|
-
}
|
|
585
|
+
}, "protocols.cancel");
|
|
568
586
|
}
|
|
569
587
|
export function requireProtocol(context, protocolId, projectId) {
|
|
570
588
|
const matches = projectId
|
|
@@ -579,7 +597,10 @@ export function requireProtocol(context, protocolId, projectId) {
|
|
|
579
597
|
candidates: matches.map((protocol) => ({ project_id: protocol.project_id, project_root: protocol.project_root }))
|
|
580
598
|
});
|
|
581
599
|
}
|
|
582
|
-
return matches[0];
|
|
600
|
+
return resolvedProtocolRecord(matches[0]);
|
|
601
|
+
}
|
|
602
|
+
function resolvedProtocolRecord(protocol) {
|
|
603
|
+
return { ...protocol, plan_path: resolveProtocolPlanPath(protocol.project_root, protocol.id, protocol.plan_path) };
|
|
583
604
|
}
|
|
584
605
|
function cancelQueueForProtocol(context, protocol, reason, force) {
|
|
585
606
|
const job = context.db.get("SELECT id, status, claimed_by_session_id FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
@@ -733,9 +754,42 @@ function releaseRelatedMergeLocks(context, protocol, reason) {
|
|
|
733
754
|
return { ok: true, released: released.length, locks: released };
|
|
734
755
|
}
|
|
735
756
|
export function readProtocolRuntimeState(context, protocol) {
|
|
757
|
+
const expected = runtimeStateJsonPath(context.ddFlowHome, protocol.project_id, protocol.id);
|
|
758
|
+
if (protocol.state_path !== expected)
|
|
759
|
+
throw new AppError("protocol_state_migration_required", "Protocol runtime path requires explicit cleanup scan/apply before use", 1, { protocol_id: protocol.id, path: protocol.state_path, expected_path: expected });
|
|
760
|
+
const accepted = latestProtocolProjection(context, protocol.project_id, protocol.id);
|
|
761
|
+
if (accepted)
|
|
762
|
+
return { state: accepted, diagnostics: [] };
|
|
763
|
+
return resolveProtocolRuntimeState(context, { ...protocol }, false);
|
|
764
|
+
}
|
|
765
|
+
/** Explicit cleanup migration; productive readers accept only the current path. */
|
|
766
|
+
export function repairProtocolRuntimeState(context, protocol) {
|
|
767
|
+
// Readers project the canonical plan path without migrating storage. Repair
|
|
768
|
+
// must start from the stored locations, not that already-normalized view.
|
|
769
|
+
const stored = context.db.get("SELECT * FROM protocols WHERE project_id = ? AND id = ?", [protocol.project_id, protocol.id]);
|
|
770
|
+
if (stored)
|
|
771
|
+
Object.assign(protocol, stored);
|
|
772
|
+
return resolveProtocolRuntimeState(context, protocol, true);
|
|
773
|
+
}
|
|
774
|
+
/** Read-only repair preparation; caller CASes paths, then persists this state. */
|
|
775
|
+
export function prepareProtocolRuntimeRepair(context, protocol) {
|
|
776
|
+
const stored = context.db.get("SELECT * FROM protocols WHERE project_id = ? AND id = ?", [protocol.project_id, protocol.id]);
|
|
777
|
+
if (!stored)
|
|
778
|
+
throw new AppError("not_found", "Protocol is not registered", 1, { protocol_id: protocol.id });
|
|
779
|
+
const state = latestProtocolProjection(context, stored.project_id, stored.id) ?? resolveProtocolRuntimeState(context, { ...stored }, false).state;
|
|
780
|
+
return {
|
|
781
|
+
state,
|
|
782
|
+
statePath: runtimeStateJsonPath(context.ddFlowHome, stored.project_id, stored.id),
|
|
783
|
+
planPath: planJsonPath(stored.project_root, stored.id),
|
|
784
|
+
previousStatePath: stored.state_path
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function resolveProtocolRuntimeState(context, protocol, repair) {
|
|
736
788
|
const diagnostics = [];
|
|
737
|
-
normalizeProtocolPlanPath(context, protocol, diagnostics);
|
|
789
|
+
normalizeProtocolPlanPath(context, protocol, diagnostics, repair);
|
|
738
790
|
try {
|
|
791
|
+
if (protocol.state_path !== runtimeStateJsonPath(context.ddFlowHome, protocol.project_id, protocol.id))
|
|
792
|
+
throw new AppError("protocol_state_migration_required", "Retired state files are not runtime authority", 1);
|
|
739
793
|
return { state: readStateFile(protocol.state_path, protocol.id), diagnostics };
|
|
740
794
|
}
|
|
741
795
|
catch {
|
|
@@ -750,7 +804,8 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
750
804
|
recommended_action: "legacy protocol state_path was moved to stable runtime storage"
|
|
751
805
|
});
|
|
752
806
|
const stablePlanPath = planJsonPath(protocol.project_root, protocol.id);
|
|
753
|
-
|
|
807
|
+
if (repair && context.db.writable)
|
|
808
|
+
context.db.run("UPDATE protocols SET state_path = ?, plan_path = ? WHERE project_id = ? AND id = ?", [stableStatePath, stablePlanPath, protocol.project_id, protocol.id]);
|
|
754
809
|
protocol.state_path = stableStatePath;
|
|
755
810
|
protocol.plan_path = stablePlanPath;
|
|
756
811
|
return { state: readStateFile(stableStatePath, protocol.id), diagnostics };
|
|
@@ -783,7 +838,7 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
783
838
|
flow_contract: flowContract,
|
|
784
839
|
updated_at: protocol.updated_at
|
|
785
840
|
};
|
|
786
|
-
if (!context.db.writable) {
|
|
841
|
+
if (!repair || !context.db.writable) {
|
|
787
842
|
return { state, diagnostics };
|
|
788
843
|
}
|
|
789
844
|
const stableStatePath = runtimeStateJsonPath(context.ddFlowHome, protocol.project_id, protocol.id);
|
|
@@ -802,9 +857,9 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
802
857
|
});
|
|
803
858
|
return { state, diagnostics };
|
|
804
859
|
}
|
|
805
|
-
function normalizeProtocolPlanPath(context, protocol, diagnostics) {
|
|
806
|
-
const canonicalPlanPath =
|
|
807
|
-
if (protocol.plan_path === canonicalPlanPath
|
|
860
|
+
function normalizeProtocolPlanPath(context, protocol, diagnostics, repair) {
|
|
861
|
+
const canonicalPlanPath = resolveProtocolPlanPath(protocol.project_root, protocol.id, protocol.plan_path);
|
|
862
|
+
if (protocol.plan_path === canonicalPlanPath)
|
|
808
863
|
return;
|
|
809
864
|
diagnostics.push({
|
|
810
865
|
code: "protocol_plan_relocated",
|
|
@@ -814,7 +869,7 @@ function normalizeProtocolPlanPath(context, protocol, diagnostics) {
|
|
|
814
869
|
source: "project_canonical_plan",
|
|
815
870
|
recommended_action: "use the project canonical plan as the sole semantic plan source"
|
|
816
871
|
});
|
|
817
|
-
if (context.db.writable) {
|
|
872
|
+
if (repair && context.db.writable) {
|
|
818
873
|
context.db.run("UPDATE protocols SET plan_path = ?, updated_at = ? WHERE project_id = ? AND id = ?", [canonicalPlanPath, context.now(), protocol.project_id, protocol.id]);
|
|
819
874
|
}
|
|
820
875
|
protocol.plan_path = canonicalPlanPath;
|
|
@@ -847,38 +902,57 @@ export function protocolRunDiagnostics(context, protocol, state) {
|
|
|
847
902
|
return { latest_run: latestWithStages, diagnostics };
|
|
848
903
|
}
|
|
849
904
|
export function persistProtocolState(context, protocol, state) {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
context.db.run(`UPDATE protocols SET
|
|
905
|
+
context.db.writeTransaction(() => {
|
|
906
|
+
context.db.run(`UPDATE protocols SET
|
|
853
907
|
status = ?, stage = ?, next_action = ?, route_json = ?, workspace_json = ?,
|
|
854
908
|
blockers_json = ?, active_def_json = ?, updated_at = ?
|
|
855
909
|
WHERE project_id = ? AND id = ?`, [
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
910
|
+
state.status,
|
|
911
|
+
state.stage,
|
|
912
|
+
state.next_action,
|
|
913
|
+
JSON.stringify(state.route),
|
|
914
|
+
JSON.stringify(state.workspace),
|
|
915
|
+
JSON.stringify(state.blockers),
|
|
916
|
+
JSON.stringify(state.active_def),
|
|
917
|
+
state.updated_at,
|
|
918
|
+
protocol.project_id,
|
|
919
|
+
protocol.id
|
|
920
|
+
]);
|
|
921
|
+
appendAudit(context, { protocolId: protocol.id, projectId: protocol.project_id, eventType: "protocol.state_projection", payload: { state } });
|
|
922
|
+
if (state.status === "closed" || state.stage === "closed") {
|
|
923
|
+
const now = context.now();
|
|
924
|
+
context.db.run(`UPDATE sessions
|
|
870
925
|
SET status = 'stopped', stop_reason = 'protocol closed', updated_at = ?, stopped_at = ?
|
|
871
926
|
WHERE project_id = ?
|
|
872
927
|
AND protocol_id = ?
|
|
873
928
|
AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [now, now, protocol.project_id, protocol.id]);
|
|
874
|
-
|
|
929
|
+
}
|
|
930
|
+
context.db.afterCommit(() => publishProtocolProjection(context, protocol.project_id, protocol.id));
|
|
931
|
+
}, "protocols.persistProtocolState");
|
|
932
|
+
}
|
|
933
|
+
function latestProtocolProjection(context, projectId, protocolId) {
|
|
934
|
+
const row = context.db.get("SELECT payload_json FROM audit_events WHERE project_id = ? AND protocol_id = ? AND event_type = 'protocol.state_projection' ORDER BY id DESC LIMIT 1", [projectId, protocolId]);
|
|
935
|
+
return row ? JSON.parse(row.payload_json).state : null;
|
|
936
|
+
}
|
|
937
|
+
export function publishProtocolProjection(context, projectId, protocolId) {
|
|
938
|
+
const protocol = requireProtocol(context, protocolId, projectId);
|
|
939
|
+
ensureDir(path.dirname(protocol.state_path));
|
|
940
|
+
withProjectionLock(`${protocol.state_path}.publish-lock`, () => {
|
|
941
|
+
const accepted = latestProtocolProjection(context, projectId, protocolId);
|
|
942
|
+
if (!accepted)
|
|
943
|
+
throw new AppError("runtime_state_missing", "Protocol has no accepted SQL state", 1, { protocol_id: protocolId });
|
|
944
|
+
const temporary = `${protocol.state_path}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
945
|
+
fs.writeFileSync(temporary, `${JSON.stringify(accepted, null, 2)}\n`);
|
|
946
|
+
fs.renameSync(temporary, protocol.state_path);
|
|
947
|
+
});
|
|
875
948
|
}
|
|
876
949
|
function findProtocol(context, protocolId, projectId) {
|
|
877
950
|
if (projectId) {
|
|
878
|
-
|
|
951
|
+
const protocol = context.db.get("SELECT * FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
952
|
+
return protocol ? resolvedProtocolRecord(protocol) : undefined;
|
|
879
953
|
}
|
|
880
954
|
const matches = context.db.all("SELECT * FROM protocols WHERE id = ? ORDER BY project_id", [protocolId]);
|
|
881
|
-
return matches.length === 1 ? matches[0] : undefined;
|
|
955
|
+
return matches.length === 1 ? resolvedProtocolRecord(matches[0]) : undefined;
|
|
882
956
|
}
|
|
883
957
|
function protocolDocumentsForProject(projectRoot) {
|
|
884
958
|
const protocolRoot = path.join(projectRoot, ".memory-bank", "protocol");
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
export const recoveryObservationBudgetMs = 120_000;
|
|
4
|
+
const activeObservation = new AsyncLocalStorage();
|
|
5
|
+
export const withRecoveryObservation = (budget, action) => activeObservation.run(budget, action);
|
|
6
|
+
export const recoveryRpcTimeout = (maximum) => {
|
|
7
|
+
const observation = activeObservation.getStore();
|
|
8
|
+
// An unbounded productive transport wait is still bounded while recovery is
|
|
9
|
+
// actively reconciling. Outside recovery, its owner policy decides when to
|
|
10
|
+
// stop rather than a hidden adapter timer.
|
|
11
|
+
if (maximum === null)
|
|
12
|
+
return observation ? observation.timeout() : null;
|
|
13
|
+
return observation?.timeout(maximum ?? 30_000) ?? maximum ?? 30_000;
|
|
14
|
+
};
|
|
15
|
+
/** Call only while holding the existing owner-claim transaction. A replay of
|
|
16
|
+
* an explicit request never replenishes time, even after another request. */
|
|
17
|
+
export function renewRecoveryObservation(snapshot, requestId) {
|
|
18
|
+
if (typeof requestId !== "string" || !requestId.trim() || requestId.length > 200)
|
|
19
|
+
throw new AppError("usage", "Reconciliation requires a nonempty request ID of at most 200 characters", 2);
|
|
20
|
+
const previous = snapshot.recovery_observation;
|
|
21
|
+
const ids = previous?.reconciliation_ids ?? [];
|
|
22
|
+
if (!Array.isArray(ids) || ids.some(id => typeof id !== "string"))
|
|
23
|
+
throw new AppError("recovery_observation_invalid", "Retained reconciliation identities are malformed", 1);
|
|
24
|
+
if (ids.includes(requestId))
|
|
25
|
+
return snapshot;
|
|
26
|
+
return { ...snapshot, recovery_observation: { budget_ms: recoveryObservationBudgetMs, remaining_ms: recoveryObservationBudgetMs, reconciliation_ids: [...ids, requestId], poll_index: 0 } };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Measures only time this owner can actually observe. Large scheduler gaps
|
|
30
|
+
* reset the sample instead of turning host sleep into evidence about a remote
|
|
31
|
+
* process. Persisting the remaining budget makes a recovered owner continue
|
|
32
|
+
* reconciliation rather than silently receiving a new unlimited wait.
|
|
33
|
+
*/
|
|
34
|
+
export function recoveryObservationBudget(saved, monotonic = () => performance.now()) {
|
|
35
|
+
const prior = saved && typeof saved === "object" ? saved.recovery_observation : null;
|
|
36
|
+
let remaining = typeof prior?.remaining_ms === "number" && Number.isFinite(prior.remaining_ms)
|
|
37
|
+
? Math.max(0, Math.min(recoveryObservationBudgetMs, prior.remaining_ms))
|
|
38
|
+
: prior == null ? recoveryObservationBudgetMs : 0;
|
|
39
|
+
let last = monotonic();
|
|
40
|
+
let pollIndex = Number.isSafeInteger(prior?.poll_index) && prior.poll_index >= 0 ? prior.poll_index : 0;
|
|
41
|
+
const observation = {
|
|
42
|
+
exhausted: () => {
|
|
43
|
+
const current = monotonic();
|
|
44
|
+
const elapsed = current - last;
|
|
45
|
+
last = current;
|
|
46
|
+
// A gap means this process was not observing; it never proves progress
|
|
47
|
+
// or death and therefore does not consume the reconciliation budget.
|
|
48
|
+
if (elapsed >= 0 && elapsed <= 60_000)
|
|
49
|
+
remaining = Math.max(0, remaining - elapsed);
|
|
50
|
+
return remaining === 0;
|
|
51
|
+
},
|
|
52
|
+
state: () => ({ budget_ms: recoveryObservationBudgetMs, remaining_ms: Math.ceil(remaining), reconciliation_ids: prior?.reconciliation_ids ?? [], poll_index: pollIndex }),
|
|
53
|
+
timeout: (maximum = 30_000) => {
|
|
54
|
+
if (observation.exhausted())
|
|
55
|
+
throw new AppError("recovery_observation_budget_exhausted", "Recovery observation budget exhausted; explicit reconciliation is required", 1);
|
|
56
|
+
return Math.max(1, Math.min(maximum, Math.ceil(remaining)));
|
|
57
|
+
},
|
|
58
|
+
nextDelay: () => Math.min([1000, 2000, 5000, 10000][Math.min(pollIndex++, 3)], Math.ceil(remaining)),
|
|
59
|
+
};
|
|
60
|
+
return observation;
|
|
61
|
+
}
|