fix(sessions): record a missed seed when it happens, not after the write

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
This commit is contained in:
Diego Imbert
2026-09-05 20:25:15 +02:00
co-authored by Claude Opus 5
parent a939a12d11
commit 82cf519606
3 changed files with 31 additions and 16 deletions
@@ -64,7 +64,10 @@ export function toolReloadEffect(name: string, args: any): ToolReloadEffect {
}
function itemPath(args: any): string | undefined {
return typeof args?.path === 'string' && args.path ? args.path : undefined
// write_trigger carries the item under `config` (see writeTriggerDraft), unlike
// every other tool here, which names it at the top level.
const p = args?.path ?? args?.config?.path
return typeof p === 'string' && p ? p : undefined
}
/** One item mutation from a chat round, as the preview needs to read it back. */
+23 -12
View File
@@ -124,6 +124,9 @@ export type ClearLiveEditorDraftOptions = UserDraftOptions & {
}
const entries = new Map<string, DraftEntry>()
// Cells whose last `seed` found no live entry (see `takeSeedMiss`). Bounded by
// the seeds that missed and not yet been read back, and each entry is one key.
const seedMisses = new Set<string>()
const liveEditorDrafts = new Map<string, LiveEditorDraft>()
/**
* Map keys whose entry should start `syncSuspended` on acquire. Lets
@@ -395,27 +398,35 @@ export const UserDraft = {
* is still needed when a write fans out across components (e.g. an
* editor's `initContent` cascading into the bound value).
*
* No-op if the entry isn't live yet (acquire via `use`/`useMany` first).
* No-op if the entry isn't live yet (acquire via `use`/`useMany` first) —
* recorded for {@link takeSeedMiss}, since the value then reached the server
* without reaching the editor that will show it.
*/
/**
* Whether a mounted editor currently holds this cell — i.e. whether {@link seed}
* would reach one rather than no-op. An editor acquires its cell only once its
* first load resolves, so this is false while one is still loading, and a
* caller that seeded then has to reconcile the editor some other way.
*/
hasLiveEntry(itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions): boolean {
return entries.has(mapKey(resolveWorkspace(opts), itemKind, path))
},
seed<V>(itemKind: UserDraftItemKind, path: string, value: V, opts?: UserDraftOptions): void {
const ws = resolveWorkspace(opts)
const mk = mapKey(ws, itemKind, path)
const entry = entries.get(mk)
if (!entry) return
if (!entry) {
seedMisses.add(mk)
return
}
seedMisses.delete(mk)
entry.seedNextWrite = true
entry.state.val = snapshotDraftValue(value)
},
/**
* Whether the last {@link seed} for this cell found no live entry, clearing the
* record. An editor acquires its cell only once its first load resolves, so a
* seed during that window reaches nothing and the value lands on the server
* alone — a caller showing that editor has to make it re-read. Recorded at the
* seed rather than asked afterwards: by then the editor may have acquired the
* cell, hiding the very miss this reports.
*/
takeSeedMiss(itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions): boolean {
return seedMisses.delete(mapKey(resolveWorkspace(opts), itemKind, path))
},
/**
* Currently-mounted live entries for `workspace` (in-tab only — for a
* workspace-wide view call `DraftService` directly).
@@ -621,10 +621,11 @@
if (pages.length === 0) return
for (const p of pages) pendingPages.add(p)
// A write reaches a hosted editor through the draft cell it holds, but an
// editor still loading holds none yet and `seed` no-opped past it — so ask
// whether the cell was live, and refresh the ones the write missed.
// editor still loading holds none yet and the seed no-opped past it. The
// miss is recorded when it happens — asking now would be too late, since
// the editor can have acquired the cell during the write's round-trip.
const kind = path ? entityKindForPage(pages[0]) : undefined
const seedReachedEditor = !kind || !path || UserDraft.hasLiveEntry(kind, path, { workspace })
const seedReachedEditor = !kind || !path || !UserDraft.takeSeedMiss(kind, path, { workspace })
const effect = effectForWrite(entity, seedReachedEditor)
if (effect !== 'none') pendingMutations.push({ pages, effect, path, workspace })
clearTimeout(reloadHandle)