@@ -83,19 +83,21 @@ function parseUrl(url: string): { mode: DemoMode; threadId: string | null } {
8383
8484/**
8585 * Decide whether to push the working itinerary to the durable checkpoint.
86- * Returns true only when a run has SETTLED (not loading), a thread exists,
87- * and the content actually changed since the last sync. The `json === lastJson`
88- * check is the echo-loop guard: hydration stamps `lastSyncedItinerary` with the
89- * incoming server JSON, so the re-fired push effect sees "no change" and skips —
90- * making hydrate→push→hydrate converge immediately.
86+ * Returns true when a thread exists and the itinerary changed since the last
87+ * sync. The `json === lastJson` check is the echo-loop guard: hydration stamps
88+ * `lastSyncedItinerary` with the incoming server JSON, so the re-fired push
89+ * effect sees "no change" and skips — making hydrate→push→hydrate converge.
90+ *
91+ * Run-state is intentionally NOT gated. The client-tool resume loop keeps the
92+ * agent loading for the whole plan, so a run-gated push never fires with the
93+ * final itinerary; instead the push attempts anyway and retries on the 409 a
94+ * mid-run `updateState` returns, until the run settles (see the push effect).
9195 */
9296export function shouldSyncCheckpoint (
93- isLoading : boolean ,
9497 threadId : string | null ,
9598 json : string ,
9699 lastJson : string ,
97100) : boolean {
98- if ( isLoading ) return false ;
99101 if ( ! threadId ) return false ;
100102 return json !== lastJson ;
101103}
@@ -259,6 +261,11 @@ export class DemoShell {
259261 effect ( ( ) => {
260262 const incoming = extractItinerary ( this . agent . value ( ) ) ;
261263 if ( incoming === null ) return ;
264+ // Don't let a behind/empty server snapshot wipe a populated local working
265+ // copy: during a plan the client tools fill the store before the checkpoint
266+ // catches up via the push below. Adopt an empty server itinerary only when
267+ // the store is itself empty (a genuine thread switch / fresh load).
268+ if ( incoming . length === 0 && untracked ( ( ) => this . itinerary . stops ( ) ) . length > 0 ) return ;
262269 const json = JSON . stringify ( incoming ) ;
263270 if ( json === this . lastSyncedItinerary ) return ;
264271 this . lastSyncedItinerary = json ;
@@ -271,23 +278,29 @@ export class DemoShell {
271278 // threadIdState(). Errors are swallowed — this is a best-effort sync.
272279 effect ( ( onCleanup ) => {
273280 const stops = this . itinerary . stops ( ) ;
274- const isLoading = this . agent . isLoading ( ) ;
275281 const tid = threadIdState ( ) ;
276282 const json = JSON . stringify ( stops ) ;
277- if ( ! shouldSyncCheckpoint ( isLoading , tid , json , this . lastSyncedItinerary ) ) return ;
278- const timer = setTimeout ( ( ) => {
279- void ( async ( ) => {
280- try {
281- await this . lgClient . threads . updateState ( tid as string , {
282- values : { itinerary : stops } ,
283- } ) ;
284- this . lastSyncedItinerary = json ;
285- } catch {
286- // best-effort: a failed checkpoint push must not break the UI.
287- }
288- } ) ( ) ;
289- } , 500 ) ;
290- onCleanup ( ( ) => clearTimeout ( timer ) ) ;
283+ if ( ! shouldSyncCheckpoint ( tid , json , this . lastSyncedItinerary ) ) return ;
284+ // Debounce, then push. A mid-run write returns 409 (the client-tool resume
285+ // loop is streaming); retry until the run settles so the FINAL itinerary
286+ // always lands in the checkpoint. onCleanup cancels a superseded attempt.
287+ let cancelled = false ;
288+ const attempt = async ( ) : Promise < void > => {
289+ if ( cancelled ) return ;
290+ try {
291+ await this . lgClient . threads . updateState ( tid as string , {
292+ values : { itinerary : stops } ,
293+ } ) ;
294+ this . lastSyncedItinerary = json ;
295+ } catch {
296+ if ( ! cancelled ) setTimeout ( ( ) => void attempt ( ) , 1500 ) ;
297+ }
298+ } ;
299+ const timer = setTimeout ( ( ) => void attempt ( ) , 1200 ) ;
300+ onCleanup ( ( ) => {
301+ cancelled = true ;
302+ clearTimeout ( timer ) ;
303+ } ) ;
291304 } ) ;
292305
293306 if ( typeof window !== 'undefined' ) {
0 commit comments