Skip to content

todo-command: prefer plugin storage over the message log - #14

Merged
aiev merged 2 commits into
aiev:masterfrom
famewolf:fix/todo-command-prefer-storage
Oct 16, 2026
Merged

aiev merged 2 commits into
aiev:masterfrom
famewolf:fix/todo-command-prefer-storage

Conversation

@famewolf

@famewolf famewolf commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem

/todo can report a stale todo list — items that were already added, completed, or removed still appear, and counts like Todo [3/3] - 0 open do not match the list the todowrite/todoread tools are actually managing.

Reproduced on 0.6.0: after rewriting the session todo list several times, /todo kept rendering an older snapshot (3/3 complete, 0 open) while the live list had five items with two still pending.

Cause

todosFromMessageSources reads the session message log first and only falls back to plugin storage:

const todos = latestTodosFromMessages(await ctx.session.context({ sessionID }))
if (todos) return todos
const record = parseTodoRecord(await ctx.storage.get(storageKey(sessionID)))

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.

const record = parseTodoRecord(await ctx.storage.get(storageKey(sessionID)))
if (record) return record.todos
const todos = latestTodosFromMessages(await ctx.session.context({ sessionID }))
return todos

The TUI strip is untouched — it still goes through latestTodosFromMessages in tui-data.ts, which remains the reader for a client that cannot reach storage.

Verification

  • npx tsc --noEmit — clean
  • npx tsx --test tests/*.test.ts — 76 passed, 0 failed

Notes

This is a behavioural change for one edge case: if storage holds a record, /todo will 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):

Todo [4/6] - 2 open
Current task: Confirm /todo renders canonical format (DOING/OPEN/DONE) from issue #3

1. [•] Confirm /todo renders canonical format (DOING/OPEN/DONE) from issue #3 — high priority
2. ☑ Memory hook fully disabled: removed from opencode.jsonc AND moved out of plugins/ auto-discovery (parked at ~/opencode/infra-memory-disabled) — high priority
3. ☑ Point config at production opencode-todolist@latest (first in plugin array) — high priority
4. ☑ Build production patch (prefer storage over message log) — tsc clean, 76/76 tests pass — high priority
5. ☑ Push patch branch fix/todo-command-prefer-storage to famewolf/opencode-todolist-fix — high priority
6. ☐ Open PR to aiev/opencode-todolist — BLOCKED: famewolf/opencode-todolist is archived; cross-fork PR needs fork relationship or un-archived fork — medium priority

After the fix it correctly showed the more recent todo here (read from the live list):

Todo [6/7] - 1 open
Current task: Open PR to aiev/opencode-todolist - blocked: famewolf fork archived, needs fork relationship
1. [x] Revert config to local todo-fork (done) — high priority
2. [x] Build production patch (prefer storage over log) + 76 tests pass — high priority
3. [x] Push patch branch to famewolf/opencode-todolist-fix — high priority
4. [x] Isolation test: memory hook removed - confirmed /todo correct (root cause: fallback shadowed plugin) — high priority
5. [x] Restore memory hook to config - done, fallback correctly stays out (55667d0) — high priority
6. [x] Confirm /todo source: DOING/OPEN never existed in code; live data correct, glyphs are UI rendering — high priority
7. [ ] Open PR to aiev/opencode-todolist - blocked: famewolf fork archived, needs fork relationship — medium priority

@famewolf famewolf closed this Oct 14, 2026
@famewolf famewolf reopened this Oct 14, 2026
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 aiev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. todowrite awaits ctx.storage.set before the call returns (src/index.ts), so a completed write can never be present in the log but missing from storage; todoread and 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 /todo is the only consumer of todosFromMessageSources, so no other reader changes behavior.
  • Fallback is safe. parseTodoRecord returns undefined for a malformed record, so the log fallback still recovers; an explicit {todos: []} now reports the empty list, consistent with todoread.
  • Verified locally: tsc clean, 78/78 tests passing. CI is re-running on 8e706de (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.

@aiev
aiev merged commit 0677429 into aiev:master Oct 16, 2026
2 checks passed
famewolf added a commit to famewolf/opencode-todolist that referenced this pull request Oct 17, 2026
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
@famewolf
famewolf deleted the fix/todo-command-prefer-storage branch October 20, 2026 06:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants