[GH-ISSUE #5896] [Bug] Rizin output limits permanently block debugger cleanup retries #3574

Closed
opened 2026-10-31 00:42:01 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5896

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

The production Rizin session manager cannot complete cleanup after its command receiver reaches an output/frame limit or command deadline. It sets protocolError, then invokes cleanup through a detach callback that immediately rejects that same state. Close and cleanup retry repeat the rejection without issuing a detach query. Even an independently observed debugger exit cannot clear the retained owner because the current inferior inventory is unknown.

A focused boundary test reproduced four cases with simulated provider streams at the manager boundary and the production stop/detach implementation: one oversized frame, total command output over 16 MiB, more than 4096 frames, and a missing completion marker. A healthy command using the same setup closed successfully. No real Rizin engine or inferior was launched.

Partial Evidence remains available on the cleanup error; the discrepancy is that the advertised partial-success result and verified automatic stop cannot occur on these branches.

Reproduction

From the source checkout with locked dependencies installed, save the test below as tests/boundary/providers/rizinCleanup.test.ts and run:

node node_modules/vitest/vitest.mjs run --project boundary \
  tests/boundary/providers/rizinCleanup.test.ts \
  --maxWorkers=1 --no-file-parallelism

The executed test used the repository's injected process and real host PTY helpers. It constructs RizinDebugSessionManager directly with platform: "linux", leaving stopProcess and both detach callbacks unchanged. The 50 ms deadline is a test setting; the production default is 120000 ms. Remove the temporary test afterwards.

import { expect, it } from "vitest";
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
import { createInjectedProcess } from "./RizinDebugSessionCoverage.fixtures.js";
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";

for (const mode of ["healthy", "oversized-frame", "output-limit", "frame-limit", "timeout"] as const) {
  it(`observes production cleanup after ${mode}`, async () => {
    let detachReads = 0;
    const injected = createInjectedProcess({ onWrite(text, control) {
      const marker = /!echo (.+)\n/u.exec(text)?.[1];
      if (!marker) return;
      const command = text.split("\n")[0];
      setImmediate(() => {
        if (command === "olj") {
          detachReads += 1;
          control.output(`[]\0${marker}\0`);
        } else if (mode === "healthy") {
          control.output(`hello\0${marker}\0`);
        } else if (mode === "oversized-frame") {
          control.output(Buffer.alloc(16 * 1024 * 1024 + 1, 97));
          control.output(`\0${marker}\0`);
        } else if (mode === "output-limit") {
          control.output(Buffer.alloc(8 * 1024 * 1024, 97));
          control.output("\0");
          setImmediate(() => {
            control.output(Buffer.alloc(8 * 1024 * 1024 + 1, 98));
            control.output(`\0${marker}\0`);
          });
        } else if (mode === "frame-limit") {
          let count = 0;
          const emit = () => {
            if (count++ < 4097) { control.output("x\0"); setImmediate(emit); }
            else control.output(`${marker}\0`);
          };
          emit();
        } else control.output("partial timeout diagnostic");
      });
    }});
    const manager = new RizinDebugSessionManager({ REA_RIZIN_COMMAND: "simulated-rizin" }, {
      platform: "linux", spawnProcess: injected.spawnProcess,
      frameTimeoutMs: mode === "timeout" ? 50 : 3000,
    });
    try {
      const started = await manager.start({ path: "fixture.bin", inferior_tty: await rizinTestInferiorTty() });
      expect(started.ok).toBe(true);
      if (!started.ok) throw started.error;
      const id = started.value.session_id;
      const result = await manager.execute(id, "fixture-command");
      if (mode === "healthy") {
        expect(result.ok).toBe(true);
        expect((await manager.close(id)).ok).toBe(true);
        expect(detachReads).toBe(1);
        expect(manager.status(id)).toBeUndefined();
        console.log(JSON.stringify({ mode, commandOk: true, closeOk: true, detachReads }));
        return;
      }
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error.cleanupIncomplete).toBe(true);
      const partial = result.error.partialObservation as { normalized_result: { output: string; output_truncated: boolean; completion_status: string } };
      expect(partial.normalized_result.output_truncated).toBe(true);
      expect(partial.normalized_result.completion_status).toBe("unknown");
      const close = await manager.close(id);
      const retry = await manager.retryCleanupFailures();
      expect(close.ok).toBe(false);
      expect(retry.ok).toBe(false);
      expect(detachReads).toBe(0);
      expect(manager.status(id)).toBeDefined();
      const stillRetained = manager.status(id);
      // Model an independently observed debugger exit; the original unknown
      // inferior inventory must not become proof that cleanup succeeded.
      injected.control.exit();
      const afterExit = await manager.close(id);
      expect(afterExit.ok).toBe(false);
      if (!afterExit.ok) expect(afterExit.error.cleanupIncomplete).toBe(true);
      await expect(manager.closeAll()).rejects.toThrow(AggregateError);
      console.log(JSON.stringify({ mode, commandOk: result.ok,
        errorTag: result.error._tag, cleanupReason: result.error.cleanup?.reason,
        partialBytes: Buffer.byteLength(partial.normalized_result.output),
        completion: partial.normalized_result.completion_status,
        closeOk: close.ok, retryOk: retry.ok, closeAll: "AggregateError",
        detachReads, retainedState: stillRetained?.state,
        afterExitOk: afterExit.ok,
        afterExitReason: afterExit.ok ? null : afterExit.error.cleanup?.reason,
      }));
    } finally {
      injected.control.exit();
      await manager.closeAll().catch(() => undefined);
    }
  });
}

All five characterization tests passed. The assertions record the current failure, rather than assert a proposed repair. The simulated exit checks that a later debugger-exit observation alone does not recover this owner; it does not claim real inferior disposition.

Expected behavior

The command contract promises retained partial output with output_truncated: true, completion_status: "unknown", and a stopped owned debugger after output/frame overflow. The implementation should have a lifecycle consistent with that promise, or clearly expose a terminal limitation and usable recovery when safe cleanup cannot be established.

Preserve the existing safety requirement: do not infer inferior identity from a PID or kill unknown targets to force success. The documented lifecycle retains ambiguous sessions for retry, while the current protocol flag prevents any such retry from reaching a fresh observation.

Impact and scope

P2: a bounded-output/deadline condition strands the session's cleanup owner and makes subsequent close/retry fail. Shutdown cannot confirm cleanup and closeAll rejects. The healthy control confirms normal completed commands can close.

The four failure modes reproduced once each in the final focused run. Live-engine resource accumulation, real-provider frequency, packaged CLI exit status, and public MCP response projection were not measured. No security-boundary crossing or target damage was observed. No general recovery for an already poisoned live session was established.

REA version

Source commit 52415975942a4b9c53a817915b313ccc404f280e; package manifest version 6.7.0. Fetched origin/main at c324d31b45fcab77e4ed7320ed60f0bf5a4ba188: the relevant manager, stop primitive, contract, adapter registration and Rizin docs are unchanged. Execution used the baseline source.

Runtime and platform

macOS 27.0.1 arm64; Node.js v24.22.0; locked Vitest 5.0.3. The exact Node 24.18.0 repository pin was not exercised. The injected provider models Linux protocol behavior on this host. Rizin is not installed on PATH, and real Linux /proc/inferior verification was not executed.

Target and artifact context

Generated ASCII output and an injected provider process. The nominal fixture.bin input is recorded but never opened or executed. A real host PTY is used for admission. No target program or real debugger runs.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

The production-manager test observed:

Case Retained partial bytes Command result Close/retry Detach queries
Healthy control 5 Success Close succeeds 1
Oversized frame 16777216 Cleanup error Both fail 0
Total output overflow 16777216 Cleanup error Both fail 0
Frame-count overflow 8191 Cleanup error Both fail 0
Command deadline 26 Cleanup error Both fail 0

Each failure is AnalysisCapabilityUnavailableError with cleanupIncomplete: true and this reason:

Owned Rizin debugger inferior detach could not be verified: Rizin protocol state is ambiguous; inferior detach is unverified.

Each retained partial observation has output_truncated: true and completion_status: "unknown". After simulated debugger exit, close still fails with:

Owned Rizin debugger inferior disposition was not verified before the debugger exited, and no owner proof confirms the identified inferior processes have ended.

closeAll rejects with AggregateError. Session status remains present and reports state: "closed"; that state follows the manager's closing flag and is not proof of process termination.

The supported entry point is the MCP tool rizin_debug_command. Its reachability is source-derived: the registered command handler dispatches to this manager and preserves partial Evidence on its error. The test did not execute an MCP transport.

Typed outcome or recovery expected

Retain partial Evidence and unknown command completion. A verified cleanup should release its exact owner and resolve cleanup uncertainty; an unsafe or impossible cleanup must preserve ownership and provide accurate terminal/recovery information. No particular new API, protocol bypass, or termination policy is proposed.

Analysis or suspected cause

Source-derived explanation, supported by the focused test:

The existing oversized-output test supplies a successful stop stub, and the usual test manager helper replaces detach with no-inferior success. Those fixtures do not exercise this conflict.

Additional context

Related reports reviewed: #5759 concerns TTY preflight diagnostics; #5090 concerns CLI exit status after fallback cleanup succeeds; #5277 concerns a retryable exit-during-detach race; #5282 concerns accepting failed post-detach readback; #4015 concerns unfinished output preservation; #5091/#5242 concern history byte bounds. PR #5516 preserves confirmed completion across cancellation/exit and leaves genuine truncation/protocol ambiguity retained; its diff does not resolve this path. #4227 is the broader lifecycle audit.

NOT_ENOUGH_INFO: introducing commit, last known good version, and occurrence with real Linux Rizin. No fix or restoration is claimed.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5896 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior The production Rizin session manager cannot complete cleanup after its command receiver reaches an output/frame limit or command deadline. It sets `protocolError`, then invokes cleanup through a detach callback that immediately rejects that same state. Close and cleanup retry repeat the rejection without issuing a detach query. Even an independently observed debugger exit cannot clear the retained owner because the current inferior inventory is unknown. A focused boundary test reproduced four cases with simulated provider streams at the manager boundary and the production stop/detach implementation: one oversized frame, total command output over 16 MiB, more than 4096 frames, and a missing completion marker. A healthy command using the same setup closed successfully. No real Rizin engine or inferior was launched. Partial Evidence remains available on the cleanup error; the discrepancy is that the advertised partial-success result and verified automatic stop cannot occur on these branches. ### Reproduction From the source checkout with locked dependencies installed, save the test below as `tests/boundary/providers/rizinCleanup.test.ts` and run: ```sh node node_modules/vitest/vitest.mjs run --project boundary \ tests/boundary/providers/rizinCleanup.test.ts \ --maxWorkers=1 --no-file-parallelism ``` The executed test used the repository's injected process and real host PTY helpers. It constructs `RizinDebugSessionManager` directly with `platform: "linux"`, leaving `stopProcess` and both detach callbacks unchanged. The 50 ms deadline is a test setting; the production default is 120000 ms. Remove the temporary test afterwards. ```ts import { expect, it } from "vitest"; import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js"; import { createInjectedProcess } from "./RizinDebugSessionCoverage.fixtures.js"; import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js"; for (const mode of ["healthy", "oversized-frame", "output-limit", "frame-limit", "timeout"] as const) { it(`observes production cleanup after ${mode}`, async () => { let detachReads = 0; const injected = createInjectedProcess({ onWrite(text, control) { const marker = /!echo (.+)\n/u.exec(text)?.[1]; if (!marker) return; const command = text.split("\n")[0]; setImmediate(() => { if (command === "olj") { detachReads += 1; control.output(`[]\0${marker}\0`); } else if (mode === "healthy") { control.output(`hello\0${marker}\0`); } else if (mode === "oversized-frame") { control.output(Buffer.alloc(16 * 1024 * 1024 + 1, 97)); control.output(`\0${marker}\0`); } else if (mode === "output-limit") { control.output(Buffer.alloc(8 * 1024 * 1024, 97)); control.output("\0"); setImmediate(() => { control.output(Buffer.alloc(8 * 1024 * 1024 + 1, 98)); control.output(`\0${marker}\0`); }); } else if (mode === "frame-limit") { let count = 0; const emit = () => { if (count++ < 4097) { control.output("x\0"); setImmediate(emit); } else control.output(`${marker}\0`); }; emit(); } else control.output("partial timeout diagnostic"); }); }}); const manager = new RizinDebugSessionManager({ REA_RIZIN_COMMAND: "simulated-rizin" }, { platform: "linux", spawnProcess: injected.spawnProcess, frameTimeoutMs: mode === "timeout" ? 50 : 3000, }); try { const started = await manager.start({ path: "fixture.bin", inferior_tty: await rizinTestInferiorTty() }); expect(started.ok).toBe(true); if (!started.ok) throw started.error; const id = started.value.session_id; const result = await manager.execute(id, "fixture-command"); if (mode === "healthy") { expect(result.ok).toBe(true); expect((await manager.close(id)).ok).toBe(true); expect(detachReads).toBe(1); expect(manager.status(id)).toBeUndefined(); console.log(JSON.stringify({ mode, commandOk: true, closeOk: true, detachReads })); return; } expect(result.ok).toBe(false); if (result.ok) return; expect(result.error.cleanupIncomplete).toBe(true); const partial = result.error.partialObservation as { normalized_result: { output: string; output_truncated: boolean; completion_status: string } }; expect(partial.normalized_result.output_truncated).toBe(true); expect(partial.normalized_result.completion_status).toBe("unknown"); const close = await manager.close(id); const retry = await manager.retryCleanupFailures(); expect(close.ok).toBe(false); expect(retry.ok).toBe(false); expect(detachReads).toBe(0); expect(manager.status(id)).toBeDefined(); const stillRetained = manager.status(id); // Model an independently observed debugger exit; the original unknown // inferior inventory must not become proof that cleanup succeeded. injected.control.exit(); const afterExit = await manager.close(id); expect(afterExit.ok).toBe(false); if (!afterExit.ok) expect(afterExit.error.cleanupIncomplete).toBe(true); await expect(manager.closeAll()).rejects.toThrow(AggregateError); console.log(JSON.stringify({ mode, commandOk: result.ok, errorTag: result.error._tag, cleanupReason: result.error.cleanup?.reason, partialBytes: Buffer.byteLength(partial.normalized_result.output), completion: partial.normalized_result.completion_status, closeOk: close.ok, retryOk: retry.ok, closeAll: "AggregateError", detachReads, retainedState: stillRetained?.state, afterExitOk: afterExit.ok, afterExitReason: afterExit.ok ? null : afterExit.error.cleanup?.reason, })); } finally { injected.control.exit(); await manager.closeAll().catch(() => undefined); } }); } ``` All five characterization tests passed. The assertions record the current failure, rather than assert a proposed repair. The simulated exit checks that a later debugger-exit observation alone does not recover this owner; it does not claim real inferior disposition. ### Expected behavior The [command contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/contracts/rizinDebugToolContracts.ts#L44-L48) promises retained partial output with `output_truncated: true`, `completion_status: "unknown"`, and a stopped owned debugger after output/frame overflow. The implementation should have a lifecycle consistent with that promise, or clearly expose a terminal limitation and usable recovery when safe cleanup cannot be established. Preserve the existing safety requirement: do not infer inferior identity from a PID or kill unknown targets to force success. The [documented lifecycle](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/reverse-engineering-tools.md#L46-L50) retains ambiguous sessions for retry, while the current protocol flag prevents any such retry from reaching a fresh observation. ### Impact and scope P2: a bounded-output/deadline condition strands the session's cleanup owner and makes subsequent close/retry fail. Shutdown cannot confirm cleanup and `closeAll` rejects. The healthy control confirms normal completed commands can close. The four failure modes reproduced once each in the final focused run. Live-engine resource accumulation, real-provider frequency, packaged CLI exit status, and public MCP response projection were not measured. No security-boundary crossing or target damage was observed. No general recovery for an already poisoned live session was established. ### REA version Source commit `52415975942a4b9c53a817915b313ccc404f280e`; package manifest version 6.7.0. Fetched `origin/main` at `c324d31b45fcab77e4ed7320ed60f0bf5a4ba188`: the relevant manager, stop primitive, contract, adapter registration and Rizin docs are unchanged. Execution used the baseline source. ### Runtime and platform macOS 27.0.1 arm64; Node.js v24.22.0; locked Vitest 5.0.3. The exact Node 24.18.0 repository pin was not exercised. The injected provider models Linux protocol behavior on this host. Rizin is not installed on PATH, and real Linux `/proc`/inferior verification was not executed. ### Target and artifact context Generated ASCII output and an injected provider process. The nominal `fixture.bin` input is recorded but never opened or executed. A real host PTY is used for admission. No target program or real debugger runs. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence The production-manager test observed: | Case | Retained partial bytes | Command result | Close/retry | Detach queries | | --- | ---: | --- | --- | ---: | | Healthy control | 5 | Success | Close succeeds | 1 | | Oversized frame | 16777216 | Cleanup error | Both fail | 0 | | Total output overflow | 16777216 | Cleanup error | Both fail | 0 | | Frame-count overflow | 8191 | Cleanup error | Both fail | 0 | | Command deadline | 26 | Cleanup error | Both fail | 0 | Each failure is `AnalysisCapabilityUnavailableError` with `cleanupIncomplete: true` and this reason: ```text Owned Rizin debugger inferior detach could not be verified: Rizin protocol state is ambiguous; inferior detach is unverified. ``` Each retained partial observation has `output_truncated: true` and `completion_status: "unknown"`. After simulated debugger exit, close still fails with: ```text Owned Rizin debugger inferior disposition was not verified before the debugger exited, and no owner proof confirms the identified inferior processes have ended. ``` `closeAll` rejects with `AggregateError`. Session status remains present and reports `state: "closed"`; that state follows the manager's closing flag and is not proof of process termination. The supported entry point is the MCP tool `rizin_debug_command`. Its reachability is source-derived: the [registered command handler](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/server/registerRizinDebugTools.ts#L70-L109) dispatches to this manager and preserves partial Evidence on its error. The test did not execute an MCP transport. ### Typed outcome or recovery expected Retain partial Evidence and unknown command completion. A verified cleanup should release its exact owner and resolve cleanup uncertainty; an unsafe or impossible cleanup must preserve ownership and provide accurate terminal/recovery information. No particular new API, protocol bypass, or termination policy is proposed. ### Analysis or suspected cause Source-derived explanation, supported by the focused test: - [Command dispatch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/rizin/RizinDebugSessionManager.ts#L625-L630) invalidates the inferior inventory before unrestricted commands. - [Truncation cleanup](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/rizin/RizinDebugSessionManager.ts#L678-L682) and [deadline cleanup](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/rizin/RizinDebugSessionManager.ts#L795-L822) call the same stop path after protocol ambiguity is recorded. The [collector limits](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/rizin/RizinDebugSessionManager.ts#L1522-L1533) set that ambiguity before returning their partial capture. - Both [detach callbacks](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/rizin/RizinDebugSessionManager.ts#L1135-L1162) reject `protocolError` before issuing `olj`. - The [shared stop routine](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/process/ownedDebuggerSession.ts#L184-L260) returns incomplete when detach fails, before direct debugger termination. Its exited-process path requires inferior-end proof, which the invalidated inventory cannot supply. - [Close/retry/shutdown](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/rizin/RizinDebugSessionManager.ts#L925-L974) reuse that path. No code clears `protocolError`. The existing oversized-output test supplies a [successful stop stub](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/tests/boundary/providers/RizinDebugSessionCoverage.part4.test.ts#L49-L78), and the usual [test manager helper](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/tests/boundary/providers/RizinTestInferiorTty.ts#L57-L90) replaces detach with no-inferior success. Those fixtures do not exercise this conflict. ### Additional context Related reports reviewed: #5759 concerns TTY preflight diagnostics; #5090 concerns CLI exit status after fallback cleanup succeeds; #5277 concerns a retryable exit-during-detach race; #5282 concerns accepting failed post-detach readback; #4015 concerns unfinished output preservation; #5091/#5242 concern history byte bounds. PR #5516 preserves confirmed completion across cancellation/exit and leaves genuine truncation/protocol ambiguity retained; its diff does not resolve this path. #4227 is the broader lifecycle audit. NOT_ENOUGH_INFO: introducing commit, last known good version, and occurrence with real Linux Rizin. No fix or restoration is claimed.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Protocol ambiguity now enters a cleanup-only synchronization path: it obtains a fresh random marker, clears protocolError only after observing that marker, then reads a fresh olj inventory through normal detach/identity/output checks. This makes retry capable of obtaining new evidence rather than rejecting forever on the old flag. The regression first withholds synchronization, requires retained cleanup and blocked work, then permits a marker and fresh empty inventory and requires close to release the owner/error.

Implementation: RizinDebugSessionManager.ts, RizinDebugSessionManager.ts.

The relevant regression checks in RizinDebugSessionCoverage.part3.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380772028 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Protocol ambiguity now enters a cleanup-only synchronization path: it obtains a fresh random marker, clears protocolError only after observing that marker, then reads a fresh olj inventory through normal detach/identity/output checks. This makes retry capable of obtaining new evidence rather than rejecting forever on the old flag. The regression first withholds synchronization, requires retained cleanup and blocked work, then permits a marker and fresh empty inventory and requires close to release the owner/error. Implementation: [RizinDebugSessionManager.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/rizin/RizinDebugSessionManager.ts#L1223), [RizinDebugSessionManager.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/rizin/RizinDebugSessionManager.ts#L1318). The relevant regression checks in [RizinDebugSessionCoverage.part3.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/providers/RizinDebugSessionCoverage.part3.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3574
No description provided.