The sequencer module — lib/sequence.mjs — runs multi-step Merus writes with rollback. If step 3 fails, steps 1 and 2 get their undo recipes invoked. If a rollback step itself fails, the run lands in a “manual cleanup needed” state.
The aai-undo CLI helps attorneys find these stuck runs and produces actionable cleanup commands. The flow:
$ aai-undo --list
Recent sequences (most recent first):
2026-05-27 14:30 ROLL mail-filing
2026-05-27 13:15 OK mail-filing
2026-05-27 10:42 UERR mail-filing
Three runs of the same label. The first rolled back cleanly. The second succeeded. The third had an undo error and needs manual cleanup.
$ aai-undo --show mail-filing
…
[third run, 10:42]
Outcome: FAILED at "step-3"
UNDO ERRORS (manual cleanup needed):
- create-task: synthetic rollback failure
SUGGESTED MANUAL CLEANUP COMMANDS:
# create-task — undo via template
node bin/merus-fetch.mjs /tasks/edit/12345 deleted=1
Looks helpful. The attorney runs the suggested command. Task 12345 gets deleted. They move on.
Except 12345 was the task ID from the most recent run, which had succeeded. The third run (the one needing cleanup) had created task 67890. The attorney just deleted the wrong task — a real task from a successful run — while the actual orphan from the failed run stayed in place.
The bug
The CLI loaded the per-run artifact (which contains the spec + result + recorded IDs) by label:
function loadRunArtifact(label) {
const files = readdirSync(SEQUENCE_RUNS_DIR);
const matches = files
.filter(f => f.endsWith(`_${safeLabel}.json`))
.sort()
.reverse();
return JSON.parse(readFileSync(matches[0])); // always the most recent
}
The comment above the function acknowledged that labels aren’t unique: “If multiple runs share a label (it can happen — labels aren’t unique), returns the most recent one.”
That comment was honest. But the calling code didn’t pass the audit entry’s timestamp — it just passed the label. So for each of the three audit entries (success, success, failed), the suggested cleanup commands all came from the same artifact: the most recent.
For the failed run, the suggested IDs were therefore wrong. For the user, the suggestion looked plausible (right command shape, right format) and they’d have no reason to verify it pointed at the orphan they actually wanted to delete.
The fix
Match the artifact to the specific run by timestamp. The artifact filenames have the form <iso-ts>_<safeLabel>.json; the audit entry has a finished_at field. Parse the filename’s ISO timestamp, find the candidate nearest to finished_at:
function loadRunArtifact(label, finishedAt = null) {
const matches = files.filter(f => f.endsWith(`_${safeLabel}.json`));
if (matches.length === 0) return null;
let chosen = matches[matches.length - 1]; // default: most recent (legacy)
if (finishedAt) {
const targetMs = Date.parse(finishedAt);
const candidates = matches.map(f => {
const m = f.match(/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z_/);
if (!m) return null;
const iso = `${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z`;
const ms = Date.parse(iso);
return isNaN(ms) ? null : { file: f, ms };
}).filter(Boolean);
candidates.sort((a, b) => Math.abs(a.ms - targetMs) - Math.abs(b.ms - targetMs));
chosen = candidates[0].file;
}
return JSON.parse(readFileSync(join(SEQUENCE_RUNS_DIR, chosen)));
}
Allows a few-ms drift between the audit’s finished_at (set by one new Date() call) and the artifact filename (set by another). Nearest-by-absolute-delta handles drift in either direction.
The caller now passes the audit entry’s timestamp:
const artifact = loadRunArtifact(s.label, s.finished_at);
Each audit entry now maps to its own artifact. The suggested cleanup commands interpolate the correct IDs for that specific run.
The synthetic verification
Two artifact files with the same label and different task IDs:
2026-05-27T15:50:00-100Z_label-test.json → task 11111
2026-05-27T15:55:00-100Z_label-test.json → task 22222
Plus two audit log entries, one matching each. Pre-fix output:
[entry 1 - 15:50]
SUGGESTED: node bin/merus-fetch.mjs /tasks/edit/22222 deleted=1 ← wrong!
[entry 2 - 15:55]
SUGGESTED: node bin/merus-fetch.mjs /tasks/edit/22222 deleted=1
Post-fix:
[entry 1 - 15:50]
SUGGESTED: node bin/merus-fetch.mjs /tasks/edit/11111 deleted=1 ✓
[entry 2 - 15:55]
SUGGESTED: node bin/merus-fetch.mjs /tasks/edit/22222 deleted=1 ✓
Why this matters
The user-facing impact: an attorney running aai-undo against a label that was reused (which is common in mail-filing where the spec label is just "file-mail-<upload-id>" per run) was getting wrong record IDs. Following the suggestion would have deleted a real task and left the actual orphan behind.
The category: silent data corruption, in a tool whose entire purpose is helping the user safely undo a previous action. The worst possible place to have a “points at the wrong thing” bug.
Caught by end-to-end testing — not code review. The function’s comment acknowledged the labels-aren’t-unique fact but the calling code didn’t use that information. Only by actually running it against multiple same-label artifacts did the consequence become visible. Same lesson as several other recent fixes: code review of plausible-looking logic isn’t enough. Run the example.