Audit: Mission Control missing task display IDs
Audit: Mission Control missing task display IDs
Date: 2026-04-08 15:42 PDT
Findings summary
- Root cause confirmed: Mission Control had active write paths that could persist tasks without
displayId. - Main UI bug:
TasksWorkspacecreates new tasks client-side withoutdisplayId, then saves the full task list withPUT /api/tasks. The server persisted the list as-is, and the client kept its local copy, so tasks could stay blank until a later refresh, or permanently if the data file was written that way. - Secondary write-path bug:
scripts/capture-task.mjswrote tasks directly todata/tasks.jsonwithout assigning adisplayId. - Safe backfill completed: 5 existing tasks missing
displayIdwere assignedJAM-34throughJAM-38. - Validation passed: storage has no missing
displayId, board view renders JAM IDs, list view renders JAM IDs, task detail modal renders the backfilled JAM ID, service rebuilt and restarted cleanly.
Root cause
There were two separate causes.
1. UI create flow bypassed numbered ID assignment
File: mission-control/components/tasks/TasksWorkspace.tsx
The create flow built a local task with createTaskId() but no displayId, then saved the entire task array through PUT /api/tasks.
2. Server list writes did not normalize missing display IDs
Files: mission-control/app/api/tasks/route.ts, mission-control/lib/tasks-store.ts
PUT /api/tasks accepted a full task list and wrote it without assigning missing displayId values. The store normalized tags and inferred project IDs, but did not backfill numbering at list-write time.
3. Capture script also bypassed numbering
File: mission-control/scripts/capture-task.mjs
The CLI/chat capture path wrote directly to data/tasks.json and did not set displayId on new tasks.
Fix
Centralized display ID normalization in the task store
File changed: mission-control/lib/tasks-store.ts
Added list-level normalization that:
- preserves every existing
displayIdexactly as stored - parses existing
JAM-<number>IDs - finds the highest existing JAM number
- assigns new JAM numbers only to tasks missing
displayId - assigns in deterministic array order
- skips collisions by tracking already-used JAM numbers
This now runs on:
readTasks(), which can self-heal older data on loadwriteTasks(), which protects all list-write pathscreateTask(), through the same numbering strategy
Fixed the UI save round-trip
Files changed:
mission-control/app/api/tasks/route.tsmission-control/components/tasks/TasksWorkspace.tsx
Changes:
PUT /api/tasksnow returns the normalized saved task list, not the raw request payload.TasksWorkspace.persistTasks()now hydrates local state from the server response, so a newly created task gets its JAM number immediately after save.
Fixed the capture script
File changed: mission-control/scripts/capture-task.mjs
Changes:
- new captured tasks now get a
displayId - numbering follows the same highest-existing-JAM-plus-one rule
- script validation now tolerates both legacy and current statuses so it can read the live task store safely
- legacy
nextinput maps toup-next,blockedmaps tobacklogwithblocked: true
Backfill performed
Data file changed: mission-control/data/tasks.json Backup created: mission-control/data/tasks.json.bak-20260408-153524
Backfilled only tasks missing displayId. Existing JAM IDs were preserved exactly. No renumbering.
Assignments made:
task-fugalg9x→JAM-34→ Improve daily notestask-iwshrvai→JAM-35→ Draft Websitetask-i7lsp651→JAM-36→ Linkedin Profile Rewritetask-mnnxai5i-vo40lk→JAM-37→ Fix for resetting only David's #david Discord sessiontask-vj7k5d2p→JAM-38→ Secure tokens
Numbering rule used:
- highest existing valid JAM ID in the store was
JAM-33 - missing tasks were assigned sequentially from
JAM-34 - non-JAM
displayIdvalues: none found - duplicate JAM IDs: none found
- remaining missing
displayIdvalues after backfill: none
Validation
Storage validation
Confirmed after backfill:
- total tasks: 29
- tasks missing
displayId: 0 - duplicate JAM IDs: 0
- non-JAM
displayIdvalues: 0 - highest persisted JAM ID:
JAM-38
API validation
Validated the exact UI-style save path by sending a full-list PUT /api/tasks payload with a temporary task missing displayId. Result:
- server assigned
JAM-39in the response - temporary validation task was removed immediately after
- final persisted store returned to max
JAM-38
Capture-path validation
Validated scripts/capture-task.mjs with a temporary task. Result:
- script assigned
JAM-34when run against the pre-backfill dataset - temporary validation task was removed before final backfill write
UI validation
Validated against the live app after rebuild/restart.
Observed:
- board view shows backfilled IDs, including
JAM-38 - list view shows backfilled IDs, including
JAM-38 - task detail modal opened via
/tasks?task=JAM-38and showed: - title:
Secure tokens - display ID:
JAM-38
Build and service refresh
npm run build: passed- Mission Control LaunchAgent restarted successfully with
./scripts/restart-mission-control.sh - live service remained available on port 3100 after restart
Notes
npm run lintstill reports pre-existing unrelated issues outside this fix:components/layout/AppShell.tsx:react-hooks/set-state-in-effectcomponents/tasks/TaskBoardColumn.tsx:react-hooks/refscomponents/tasks/TaskListView.tsx:react-hooks/refs- warnings in
lib/mc2/timeline.tsandscripts/migrate-projects.mjs - These did not block build or this fix.
- Scope kept tight to numbering and immediate data/UI effects.
Exact files changed
/Users/vinny/.openclaw/workspace/mission-control/lib/tasks-store.ts/Users/vinny/.openclaw/workspace/mission-control/app/api/tasks/route.ts/Users/vinny/.openclaw/workspace/mission-control/components/tasks/TasksWorkspace.tsx/Users/vinny/.openclaw/workspace/mission-control/scripts/capture-task.mjs/Users/vinny/.openclaw/workspace/mission-control/data/tasks.json
Ready for review
Yes