Repository navigation
todo-command: prefer plugin storage over the message log - #14
Conversation
Cover the inverted priority directly: storage wins whenever a stored record exists (even when the log's write timestamp is newer), the log still recovers when the record is unusable, and storage is used when the log has no completed todowrite. Renames the old 'falls back to same-plugin storage' test, which still described the previous ordering.
aiev
left a comment
There was a problem hiding this comment.
Maintainer review
Verified the change against the current head (8e706de, including the test commit I pushed to the branch, since editing from maintainers is allowed):
- Write ordering holds.
todowriteawaitsctx.storage.setbefore the call returns (src/index.ts), so a completed write can never be present in the log but missing from storage;todoreadand the context hook already read that same record — the command now genuinely reports "the same list the todowrite/todoread tools manage". - Scope is right. The TUI strip still reads the log via
latestTodosFromMessages(src/tui-data.ts), and/todois the only consumer oftodosFromMessageSources, so no other reader changes behavior. - Fallback is safe.
parseTodoRecordreturnsundefinedfor a malformed record, so the log fallback still recovers; an explicit{todos: []}now reports the empty list, consistent withtodoread. - Verified locally:
tscclean, 78/78 tests passing. CI is re-running on8e706de(Node 20 and 22).
One argument worth adding to the PR description: with a project auto-todos.json, storage and the log can legitimately diverge — withAutoTodos merges config entries into the list that gets stored, while the log keeps the raw model input (tool.state.input). Under the old ordering, an auto-todos session could render a list that never matched storage even without compaction, which strengthens the case beyond the reproduced staleness.
Review gap addressed in 8e706de
The suite did not pin the new precedence — no test had both sources present and disagreeing, and "falls back to same-plugin storage" still described the previous ordering. The added commit:
- pins that storage wins when both sources hold a list, even when the log's timestamp is newer (the exact scenario this fix targets);
- pins that the log still recovers when the stored record is unusable;
- renames the storage test to reflect its primary-source role.
The edge case from the PR notes stands: an externally cleared storage with a surviving log now reports the log's list. That is the correct direction — session.deleted removes the record on the normal lifecycle path, and independent clears are not something the command can detect. Recommend a patch release after merge since this is a user-visible behavior fix.
PR aiev#14 (todo-command: prefer plugin storage over the message log) merged upstream 2026-10-16 as aiev/opencode-todolist 0.6.1 (squash 0677429). Fork is no longer needed: - local config repointed from the vendored fixed copy to npm opencode-todolist@0.6.1 - the fixed copy was retired to /home/famewolf/opencode/attic (2026-10-17) Branches retained read-only in the archived repo: - fix/todo-command-prefer-storage: squash-merged upstream (original SHAs not in upstream history) - docs/readme-install-update-uninstall (04af485): fork-local README docs (compat floor, updating, uninstall), written for the fixed-copy era; never upstreamed, kept for the record
Problem
/todocan report a stale todo list — items that were already added, completed, or removed still appear, and counts likeTodo [3/3] - 0 opendo not match the list thetodowrite/todoreadtools are actually managing.Reproduced on 0.6.0: after rewriting the session todo list several times,
/todokept rendering an older snapshot (3/3 complete, 0 open) while the live list had five items with two still pending.Cause
todosFromMessageSourcesreads the session message log first and only falls back to plugin storage:The message log is a lossy projection of the same data. It is subject to compaction and retention, and its timestamps can order writes differently from the order they actually completed. Whenever the log and storage disagree, the command prefers the lossy copy.
That also contradicts the command's own description — "Same list the todowrite/todoread tools manage" — because both of those tools read and write plugin storage, not the log. The log reader exists for the TUI strip, which cannot read plugin storage; the server-side command has no such constraint.
Fix
Invert the priority: plugin storage is authoritative for the server-side command, with the log kept as a fallback so the command still works before any storage record exists.
The TUI strip is untouched — it still goes through
latestTodosFromMessagesintui-data.ts, which remains the reader for a client that cannot reach storage.Verification
npx tsc --noEmit— cleannpx tsx --test tests/*.test.ts— 76 passed, 0 failedNotes
This is a behavioural change for one edge case: if storage holds a record,
/todowill now report that instead of a newer list recovered from the log. That is the intended direction — storage is what the tools use — but it is worth calling out in case anyone depended on the log reader recovering a list after storage was cleared independently.Before / after (lists actually hit in live sessions)
Before the fix the todo looked like this (read from the message log):
After the fix it correctly showed the more recent todo here (read from the live list):