Skip to content

Combine saved decision answers into one Planner turn and keep them across a restart - #719

Merged
lavaman131 merged 1 commit into
mainfrom
combined-durable-answer-turns
Oct 21, 2026
Merged

lavaman131 merged 1 commit into
mainfrom
combined-durable-answer-turns

Conversation

@lavaman131

Copy link
Copy Markdown
Collaborator

Follows #712, which made each saved answer to a decision card the Planner left open (open_decisions / open_decision) start one Planner turn. That PR reported two gaps: answering six cards started six Planner turns, and an answer that found no Planner waited only in memory, so a restart dropped it.

What changes

One turn for answers saved together. Answers saved to the same document close together go into one Planner turn. That turn lists every answer: the question, the saved answer, the chosen option and its description, and who saved it.

  • The turn starts after 5 s with no new save, or 30 s after the first save, whichever comes first.
  • It never starts while a Planner turn runs or waits in that document's Chat. Answers saved during that time join the next turn.
  • Chat shows one notice: @ana answered "…" for a single answer, or @ana and @lee answered 3 decisions for several.
  • One turn carries at most 20 answers; the rest go in the turn after it.

Answers survive a restart. Every answer is written to storage first, in the new decision_answers table (migration 027, plus the memory adapter and a shared contract test). There is one row per document and decision, holding the saved generation, the save time, turn_id and delivered_at.

  • ownerAvailable (an owner appears, or a Planner attaches) delivers whatever is waiting. That includes answers saved before a restart.
  • Saving the same card again stores a newer generation, which replaces the waiting row.
  • Reopening a card makes its answer stale, so it is never handed over.
  • A delivered row stays marked, so reporting the same generation again changes nothing.

No double delivery after a crash.

  1. Before a turn starts, its answers are claimed under a fresh ULID (turn_id).
  2. That ULID becomes the id of the turn's Chat notice (Chat.Instruction.notice). Chat saves the notice before the turn runs.
  3. After Chat accepts the turn, the rows are marked delivered.

If the process dies between steps 2 and 3, the next process re-sends the claimed rows under the same id. serverInstruction finds that the Chat transcript already holds a notice with that id, reports the turn as accepted, and the rows are marked delivered without starting a second turn.

Unchanged: answers that a blocking ask or a workflow's question is waiting for still go straight back to the asker and start no turn. Answer turns still run only under the existing owner, and under HARNESS=remote only while a Planner is attached.

The connector is not changed.

Tests

Written to fail first (answer-turns.test.ts, against real Chat and serverInstruction):

  • Six answers saved in a burst start one turn that lists all six, with the notice "@ana answered 6 decisions". Before this change they started six.
  • Answers that keep arriving go in one turn once the first has waited the longest time.
  • An answer saved while a Planner turn runs joins the next turn.
  • Answers that found no Planner are delivered once by a fresh process over the same storage after a restart, and never again by a third.
  • A crash between Chat accepting the turn and marking it delivered starts no second turn. Disabling the notice-id check in serverInstruction makes this test fail.
  • Saving again replaces a waiting answer; reopening cancels it.
  • Blocking ask and workflow answers start no turn (unchanged).

Storage contract tests, run against both memory and PostgreSQL: newest generation wins; claim, release and delivered; documents kept separate; rows removed with the document.

…ross a restart

Answering six cards the Planner left open started six Planner turns, and
an answer that found no Planner waited only in memory, so a restart
dropped it.

Answers saved to one document close together now go in one Planner turn
that lists each question, saved answer, chosen option and who saved it.
The turn starts once nobody has saved another answer for five seconds,
or 30 seconds after the first, and never while a Planner turn runs or
waits in that Chat, so answers saved meanwhile join the next turn. Chat
shows one notice, such as "@ana answered 3 decisions".

Each answer is stored first in a new decision_answers table (and the
memory adapter), one row per document and decision, so answers that
found no Planner are delivered once an owner appears or a Planner
attaches, after a restart too. Saving again replaces a waiting answer
and reopening makes it stale. A turn's answers are claimed under a fresh
id before it starts and marked delivered once Chat accepts it; its Chat
notice takes that id, so answers claimed before a crash but not yet
marked are recognized as already started and never run twice. Answers a
blocking ask or a workflow's question waits for still go straight back
and start no turn.

Assistant-model: Claude Opus 5.5 (fast)
Assistant-workflow: inline (fast subagents)
Assistant-verification: bun test (unit, domain and memory storage)
Assistant-verification: PostgreSQL storage contract and migration tests
Assistant-verification: bun run types and bun run ci
Assistant-verification: Chromium decision, hosted, connector and experiment suites
Co-authored-by: Alex Lavaee <alexlavaee@microsoft.com>
@lavaman131 lavaman131 self-assigned this Oct 21, 2026
@lavaman131
lavaman131 merged commit 9838512 into main Oct 21, 2026
46 of 48 checks passed
@lavaman131
lavaman131 deleted the combined-durable-answer-turns branch October 21, 2026 22:03
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.

1 participant