docs(cursor): scope the collision claim to task-targeted ops
The refusal rationale overstated the blast radius: `getTaskTargets` falls back to a phase's tasks, then to every task, and only consults `findTaskByContent` when an op names a `task`. So a content collision strands the second row for task-targeted `done`/`drop`/`rm` only -- phase-wide and untargeted ops still reach both. The guard is unchanged; only the source comment, test comment, and changelog entry were overstating why it exists.
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
- Fixed a server-resolved Cursor todo call leaving its transcript block stuck pending: the synthetic completion was emitted under a freshly generated id instead of the streamed call id the interactive transcript filed the block under, so the card animated indefinitely. The settled call id is now handed to the sync handler.
|
||||
- Fixed server-resolved Cursor todo blocks disappearing from rebuilt transcripts: nothing produced a `toolResult` for them, and `buildSessionContext` strips any `toolCall` left unpaired, so the interaction vanished on reload, branch switch, or transcript rebuild. The result the host builds is now persisted verbatim — it carries the `details.phases` the todo renderer rebuilds the list from, which a summary-only result would have replayed as `0 tasks`.
|
||||
- Fixed a refused or failed Cursor todo call leaving its card animating forever. Only a successful snapshot settled the block, so a `read_todos` narrowed by a filter and a server `UpdateTodosError` both went unanswered — no `tool_execution_end`, and no `toolResult` to keep the block from being stripped on rebuild. Every completed native todo call now settles. A server error is carried through as a failed result rather than collapsed into the benign "nothing to mirror" case, which would have replayed the failure as a success.
|
||||
- Hardened Cursor todo mirroring against snapshots whose rows collide on content. Cursor's wire model identifies todos by `id` and can represent two rows sharing the same text; the local list is keyed by content alone and the `todo` tool rejects a duplicate outright, so importing such a pair would leave every later `done`/`drop`/`rm` resolving to the first row and the second permanently unaddressable. The snapshot is now refused like any other that cannot be represented locally — local state is left untouched and the call still settles as a no-op.
|
||||
- Hardened Cursor todo mirroring against snapshots whose rows collide on content. Cursor's wire model identifies todos by `id` and can represent two rows sharing the same text; the local list is keyed by content alone and the `todo` tool rejects a duplicate outright, so importing such a pair would leave every task-targeted `done`/`drop`/`rm` resolving to the first row and the second unreachable. The snapshot is now refused like any other that cannot be represented locally — local state is left untouched and the call still settles as a no-op.
|
||||
|
||||
## [17.1.3] - 2026-07-24
|
||||
|
||||
|
||||
@@ -2151,9 +2151,9 @@ function extractTodoSnapshot(toolCall: CursorTodoToolCall): CursorTodoSnapshot |
|
||||
// The wire model identifies rows by `id` and can represent two rows sharing
|
||||
// `content`; the local list is keyed by content alone (`findTaskByContent`)
|
||||
// and `todo` rejects a duplicate outright. Importing such a snapshot would
|
||||
// leave every later `done`/`drop`/`rm` resolving to the first row and the
|
||||
// second permanently unaddressable, so it is refused like any other snapshot
|
||||
// that cannot be represented locally.
|
||||
// leave every task-targeted `done`/`drop`/`rm` resolving to the first row and
|
||||
// the second unreachable (phase-wide and untargeted ops still hit both), so
|
||||
// it is refused like any other snapshot that cannot be represented locally.
|
||||
const seen = new Set<string>();
|
||||
for (const todo of mapped) {
|
||||
if (seen.has(todo.content)) return null;
|
||||
|
||||
@@ -489,7 +489,8 @@ describe("cursor native todo bridge (wire-encoded protobuf)", () => {
|
||||
// The wire model identifies rows by `id` and can represent two rows with
|
||||
// the same `content`, so the bridge must survive one. The local list is
|
||||
// keyed by content alone, so importing the pair would make every later
|
||||
// `done`/`drop` resolve to the first row and strand the second.
|
||||
// task-targeted `done`/`drop` resolve to the first row and strand the
|
||||
// second.
|
||||
//
|
||||
// Positive control: the same two rows with distinct content do sync, so
|
||||
// the refusal below is attributable to the collision.
|
||||
|
||||
Reference in New Issue
Block a user