Why
Todo lists drift silently. todowrite is the only write path (packages/opencode/src/session/todo.ts — delete-all + reinsert, publishes Event.Updated), but once written nothing ever re-surfaces the list to the model: completed work stays pending, stale items linger, and in_progress is never set. The model needs the current list re-injected while uncompleted items exist, or it loses track after a few steps.
Confirmed direction
Original proposal: inject the todo snapshot via PreToolUse additionalContexts, skip on todowrite calls, skip when all done. Exploration refined the seam, keeping the intent:
PreToolUseadditionalContexts are prepended to each tool result (packages/opencode/src/session/tools.ts:188-192), so N parallel tool calls would duplicate the reminder N times in one batch, and pure-reasoning steps (no tool calls) would never see it — exactly where drift is worst.- Instead, inject once per model step through the existing SessionReminders seam (
packages/opencode/src/session/prompt.ts:1682 → packages/opencode/src/session/reminders.ts pattern): a synthetic text part on the last user message, in-memory only (model-visible, never persisted — no transcript bloat).
Skip conditions (user-confirmed, translated to the per-step seam):
- No todos for the session → no injection.
- All todos completed → no injection.
- Freshness guard (replaces the skip-on-todowrite condition): the previous step ended with a successful
todowrite in this session → the list was just updated by the model, skip this step.
Scope
- New todo reminder module (sibling of SessionReminders) reading
Todo.Service.get(sessionID) at step start. - Compact single-line-per-item rendering (status + content); only uncompleted items or the full list with statuses — pick the smaller stable rendering.
- Applies to any session holding todos, including child/subagent sessions (
todowrite is not in ROOT_ONLY_TOOLS). - Freshness signal: track the last successful
todowrite step per session (via tool execution or Event.Updated subscription). - Tests: no todos → no part; all completed → no part; pending items → exactly one part per model step with correct statuses; freshness skip after a todowrite step; parallel tool calls yield at most one reminder per model request; reminder parts are not persisted to the transcript; behavior survives compaction.
Acceptance
- With uncompleted todos, every model step (including tool-free steps) carries at most one todo reminder reflecting current statuses.
- After a
todowrite step, the immediately following step carries no reminder. - All-completed and empty lists produce no reminders.
- No duplicate reminders from parallel tool calls.
- Transcript storage unchanged (reminder is model-visible only, not durable).
- Focused session-tool tests and
bun typecheck pass from packages/opencode.
Out of scope
- Requiring users to configure
hooks.json (this is product-native; users may still layer their own hooks). - Auto-mutating todo state — the reminder informs; the model still owns updates via
todowrite. - Persistence schema changes or new events.
- DAG node child-session-specific behavior beyond per-session applicability.
Delivery
Separate WHY from #386-#388 (session tooling, not DAG transport). Bind to its own delivery with specgit issue <number> when implementation starts; created via gh directly to avoid clobbering the active end-structured-output delivery record.
Why
Todo lists drift silently.
todowriteis the only write path (packages/opencode/src/session/todo.ts— delete-all + reinsert, publishesEvent.Updated), but once written nothing ever re-surfaces the list to the model: completed work stays pending, stale items linger, andin_progressis never set. The model needs the current list re-injected while uncompleted items exist, or it loses track after a few steps.Confirmed direction
Original proposal: inject the todo snapshot via
PreToolUseadditionalContexts, skip ontodowritecalls, skip when all done. Exploration refined the seam, keeping the intent:PreToolUseadditionalContextsare prepended to each tool result (packages/opencode/src/session/tools.ts:188-192), so N parallel tool calls would duplicate the reminder N times in one batch, and pure-reasoning steps (no tool calls) would never see it — exactly where drift is worst.packages/opencode/src/session/prompt.ts:1682→packages/opencode/src/session/reminders.tspattern): a synthetic text part on the last user message, in-memory only (model-visible, never persisted — no transcript bloat).Skip conditions (user-confirmed, translated to the per-step seam):
todowritein this session → the list was just updated by the model, skip this step.Scope
Todo.Service.get(sessionID)at step start.todowriteis not inROOT_ONLY_TOOLS).todowritestep per session (via tool execution orEvent.Updatedsubscription).Acceptance
todowritestep, the immediately following step carries no reminder.bun typecheckpass frompackages/opencode.Out of scope
hooks.json(this is product-native; users may still layer their own hooks).todowrite.Delivery
Separate WHY from #386-#388 (session tooling, not DAG transport). Bind to its own delivery with
specgit issue <number>when implementation starts; created viaghdirectly to avoid clobbering the activeend-structured-outputdelivery record.