From f3a664c0b6b6941c8b3f51af96dff04702f71f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 3 Oct 2026 10:58:59 +0200 Subject: [PATCH 1/2] fix: preserve capture records and discard retired settle observations --- src/daemon/__tests__/generic-settle.test.ts | 101 ++++++++++++++++-- .../selector-capture-runtime.test.ts | 45 ++++++++ src/daemon/__tests__/session-snapshot.test.ts | 40 ++++++- .../snapshot-command-runtime.test.ts | 13 ++- src/daemon/generic-settle.ts | 2 + .../interaction-ios-tap-outcome.test.ts | 29 +++++ .../internal/interaction-ios-tap-outcome.ts | 14 ++- .../interaction-touch-reference-frame.ts | 4 +- src/daemon/selector-capture-runtime.ts | 5 +- src/daemon/session-snapshot.ts | 3 +- src/daemon/snapshot-command-runtime.ts | 18 ++-- 11 files changed, 244 insertions(+), 30 deletions(-) diff --git a/src/daemon/__tests__/generic-settle.test.ts b/src/daemon/__tests__/generic-settle.test.ts index 5ae2c10d04..033cca5c5a 100644 --- a/src/daemon/__tests__/generic-settle.test.ts +++ b/src/daemon/__tests__/generic-settle.test.ts @@ -13,7 +13,7 @@ import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import { setSessionSnapshot } from '../session-snapshot.ts'; import type { SessionStore } from '../session-store.ts'; import type { DaemonRequest, DaemonResponse } from '../daemon-request.ts'; -import type { SessionState } from '../session-state.ts'; +import type { SessionRef, SessionState } from '../session-state.ts'; import { buildSnapshotState } from '@agent-device/capture-kit/snapshot-state'; // #1638 `--settle` on the GENERIC daemon route (scroll/back): the settled diff, @@ -85,19 +85,19 @@ type SettlePayload = { const captureObservations: Array<{ postGestureStabilizationPending: boolean }> = []; async function emulateCaptureSnapshotForSession( - session: SessionState, + ref: SessionRef, flags: CommandFlags | undefined, sessionStore: SessionStore, options: { interactiveOnly: boolean }, ) { captureObservations.push({ - postGestureStabilizationPending: session.postGestureStabilization !== undefined, + postGestureStabilizationPending: + sessionStore.requireCurrent(ref).postGestureStabilization !== undefined, }); const effectiveFlags = { ...(flags ?? {}), snapshotInteractiveOnly: options.interactiveOnly }; const snapshotData = (await mockDispatch('snapshot')) as Parameters[0]; const snapshot = buildSnapshotState(snapshotData ?? {}, effectiveFlags); - setSessionSnapshot(session, snapshot); - sessionStore.set(session.name, session); + setSessionSnapshot(sessionStore.requireCurrent(ref), snapshot); return snapshot; } @@ -210,8 +210,7 @@ beforeEach(() => { mockCaptureSnapshotForSession.mockImplementation( (...args: Parameters) => { const [ref, flags, sessionStore, _contextFromFlags, options] = args; - const session = sessionStore.requireCurrent(ref); - return emulateCaptureSnapshotForSession(session, flags, sessionStore, options); + return emulateCaptureSnapshotForSession(ref, flags, sessionStore, options); }, ); }); @@ -620,3 +619,91 @@ test('an orphaned --settle-quiet is rejected before the command dispatches', asy expect(response.error?.code).toBe('INVALID_ARGS'); expect(response.error?.message).toContain('--settle-quiet'); }); + +test('scroll stays successful when its lifetime retires during optional settle observation', async () => { + const store = makeSessionStore(); + const name = 'generic-settle-retired'; + const session = seedSession(name, store); + const ref = store.lookup(name)!; + mockCommandDispatch([AFTER_NODES]); + mockCaptureSnapshotForSession.mockImplementationOnce( + async (boundRef, flags, sessionStore, _context, options) => { + const observed = await emulateCaptureSnapshotForSession( + boundRef, + flags, + sessionStore, + options, + ); + queueMicrotask(() => { + store.retire(ref); + store.publish(name, makeIosSession(name)); + }); + return observed; + }, + ); + const response = await dispatchGeneric({ + sessionName: name, + sessionStore: store, + session, + command: 'scroll', + positionals: ['down'], + flags: { ...SETTLE_FLAGS }, + }); + expect(expectOkData(response).settle).toBeUndefined(); + expect(store.get(name)?.snapshot).toBeUndefined(); + expect(store.get(name)?.actions).toEqual([]); +}); + +for (const change of ['rebuild', 'retire'] as const) { + test(`held generic settle capture respects its lifetime after ${change}`, async () => { + const store = makeSessionStore(); + const name = `generic-held-settle-${change}`; + const session = seedSession(name, store); + const ref = store.lookup(name)!; + let entered!: () => void; + let release!: () => void; + const reached = new Promise((resolve) => { + entered = resolve; + }); + const held = new Promise((resolve) => { + release = resolve; + }); + mockDispatch.mockImplementation(async (command) => { + if (command !== 'snapshot') return {}; + entered(); + await held; + return snapshotPayload({ nodes: AFTER_NODES }); + }); + const pending = dispatchGeneric({ + sessionName: name, + sessionStore: store, + session, + command: 'scroll', + positionals: ['down'], + flags: { ...SETTLE_FLAGS }, + }); + try { + await reached; + const trace = { outPath: 'latest-trace', startedAt: 1 }; + const successor = makeIosSession(name); + if (change === 'rebuild') store.update(ref, { trace }); + else { + store.retire(ref); + store.publish(name, successor); + } + release(); + const data = expectOkData(await pending); + if (change === 'rebuild') { + expect(store.requireCurrent(ref).trace).toBe(trace); + expect(store.requireCurrent(ref).snapshot?.nodes[1]?.label).toBe('Load more'); + } else { + expect(data.settle).toBeUndefined(); + expect(store.get(name)).toBe(successor); + expect(successor.snapshot).toBeUndefined(); + } + } finally { + release(); + await pending; + } + }); +} diff --git a/src/daemon/__tests__/selector-capture-runtime.test.ts b/src/daemon/__tests__/selector-capture-runtime.test.ts index d90131a822..9230151055 100644 --- a/src/daemon/__tests__/selector-capture-runtime.test.ts +++ b/src/daemon/__tests__/selector-capture-runtime.test.ts @@ -377,3 +377,48 @@ test('a held selector capture updates the matching rebuilt record without restor await running.catch(() => {}); } }); + +test('selector capture and sparse recovery both use current same-lifetime app metadata', async () => { + const { runtime, sessionName, sessionStore } = makeCaptureRuntime('selector-current-metadata'); + const ref = sessionStore.lookup(sessionName)!; + sessionStore.update(ref, { appBundleId: 'before-first-capture' }); + boundCapture + .mockImplementationOnce(async () => { + sessionStore.update(ref, { appBundleId: 'before-recovery-capture' }); + return { + backend: 'xctest', + producer: 'apple-runner', + nodes: [{ index: 0, type: 'Application' }], + }; + }) + .mockResolvedValueOnce({ + backend: 'xctest', + producer: 'apple-runner', + nodes: [{ index: 0, type: 'Button', label: 'Recovered' }], + }); + const result = await runtime.capture({ + flags: { snapshotInteractiveOnly: true }, + recovery: { legacyIosSparse: { query: 'Search', shouldScope: false } }, + }); + expect(result.snapshot.nodes[0]?.label).toBe('Recovered'); + expect(boundCapture.mock.calls.map(([input]) => input.options?.appBundleId)).toEqual([ + 'before-first-capture', + 'before-recovery-capture', + ]); + expect(sessionStore.requireCurrent(ref).appBundleId).toBe('before-recovery-capture'); +}); + +test('a retired selector runtime refuses even a reusable cached capture without touching its successor', async () => { + const { runtime, sessionName, sessionStore } = makeCaptureRuntime('selector-retired-cache'); + const ref = sessionStore.lookup(sessionName)!; + await runtime.capture({ flags: {} }); + sessionStore.retire(ref); + const successor = sessionStore.publish(sessionName, ref.session); + await expect(runtime.capture({ flags: {} })).rejects.toThrow( + expect.objectContaining({ + details: expect.objectContaining({ reason: 'session_lifetime_ended' }), + }), + ); + expect(boundCapture).toHaveBeenCalledOnce(); + expect(sessionStore.requireCurrent(successor)).toBe(ref.session); +}); diff --git a/src/daemon/__tests__/session-snapshot.test.ts b/src/daemon/__tests__/session-snapshot.test.ts index c7b6fa2458..41c9ee2002 100644 --- a/src/daemon/__tests__/session-snapshot.test.ts +++ b/src/daemon/__tests__/session-snapshot.test.ts @@ -1,8 +1,11 @@ import { expect, test } from 'vitest'; +import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts'; +import type { SettleObservation } from '@agent-device/contracts/interaction'; import type { SnapshotState } from '@agent-device/kernel/snapshot'; import type { SessionState } from '../session-state.ts'; import { markSessionPartialRefsIssued, + issueSettleRefs, resolveRefStalenessWarning, setSessionSnapshot, setCommandSnapshot, @@ -179,12 +182,14 @@ test('a ref pinned before a diff keeps resolving: the diff advances the counter, // `diff` replaces the stored tree, so lineage advances the counter — but it passes // `issuesRefsToClient: false`, so it never reactivates the frame. const afterDiff: SessionState = { ...session }; + const diffSnapshot = makeSnapshot(); setCommandSnapshot(afterDiff, { - snapshot: afterDiff.snapshot!, + snapshot: diffSnapshot, scopeSource: undefined, keptCurrentSnapshot: false, previousGeneration: session.snapshotGeneration, }); + expect(afterDiff.snapshot).toBe(diffSnapshot); expect(afterDiff.snapshotGeneration).not.toBe(session.snapshotGeneration); expect(refFrameEpoch(afterDiff)).toBe(issuedAt); @@ -220,3 +225,36 @@ test('keeping the current snapshot leaves the counter alone', () => { }); expect(session.snapshotGeneration).toBe(before); }); + +for (const retire of [false, true]) { + test(`settle ref issuance checks lifetime before publishing (${retire ? 'retired' : 'live'})`, () => { + const store = makeSessionStore(); + const session = makeSession(); + setSessionSnapshot(session, makeSnapshot()); + const ref = store.publish('cwd:settle:default', session); + const priorFrame = refFrame(session); + if (retire) { + store.retire(ref); + store.publish(ref.address, session); + } + const observation: SettleObservation = { + settled: true, + waitedMs: 1, + captures: 2, + quietMs: 1, + timeoutMs: 20, + diff: { + summary: { additions: 1, removals: 0, unchanged: 0 }, + lines: [{ kind: 'added', text: 'new button', ref: 'e1' }], + }, + }; + const generation = issueSettleRefs(ref, store, observation); + if (retire) { + expect(generation).toBeUndefined(); + expect(refFrame(session)).toBe(priorFrame); + } else { + expect(generation).toBe(session.snapshotGeneration); + expect(refFrameScope(session)).toEqual(new Set(['e1'])); + } + }); +} diff --git a/src/daemon/__tests__/snapshot-command-runtime.test.ts b/src/daemon/__tests__/snapshot-command-runtime.test.ts index 2a17a1d6ea..80002fcb8d 100644 --- a/src/daemon/__tests__/snapshot-command-runtime.test.ts +++ b/src/daemon/__tests__/snapshot-command-runtime.test.ts @@ -86,7 +86,7 @@ for (const command of ['snapshot', 'diff snapshot'] as const) { }); } -for (const change of ['rebuild', 'replace'] as const) { +for (const change of ['unchanged', 'rebuild', 'replace'] as const) { test(`snapshot completion respects a scoped lifetime after ${change}`, async () => { const sessionStore = makeSessionStore(); const address = 'cwd:snapshot-completion:default'; @@ -117,13 +117,18 @@ for (const change of ['rebuild', 'replace'] as const) { ); try { await entered.promise; - if (change === 'rebuild') { - const trace = { outPath: 'intervening-trace', startedAt: 1 }; - sessionStore.update(ref, { trace }); + if (change !== 'replace') { + const trace = + change === 'rebuild' ? { outPath: 'intervening-trace', startedAt: 1 } : ref.session.trace; + if (change === 'rebuild') sessionStore.update(ref, { trace }); + const priorRecord = sessionStore.requireCurrent(ref); release.resolve(); expect(await result).toMatchObject({ response: { ok: true } }); const current = sessionStore.requireCurrent(ref); expect(current.trace).toBe(trace); + expect(current).not.toBe(priorRecord); + expect(priorRecord.snapshot).toBeUndefined(); + expect(ref.session.snapshot).toBeUndefined(); expect(current.snapshot?.nodes[0]?.label).toBe('Captured'); } else { sessionStore.retire(ref); diff --git a/src/daemon/generic-settle.ts b/src/daemon/generic-settle.ts index 70e1ead5d6..9bd5d09a63 100644 --- a/src/daemon/generic-settle.ts +++ b/src/daemon/generic-settle.ts @@ -110,6 +110,8 @@ async function observeSettled( session: context.sessionName, requestId: context.req.meta?.requestId, }); + if (context.sessionRef && !context.sessionStore.resolveCurrent(context.sessionRef)) + return undefined; const refsGeneration = issueSettleRefs(context.sessionRef, context.sessionStore, observation); return refsGeneration === undefined ? observation : { ...observation, refsGeneration }; } diff --git a/src/daemon/interaction/internal/__tests__/interaction-ios-tap-outcome.test.ts b/src/daemon/interaction/internal/__tests__/interaction-ios-tap-outcome.test.ts index d0163f192d..30ed2bf4b5 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-ios-tap-outcome.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-ios-tap-outcome.test.ts @@ -745,3 +745,32 @@ test('corroborated runtime taps retain target evidence through save and replay', expect(pressCount).toBe(2); expect(snapshotCount).toBeGreaterThanOrEqual(4); }); + +test('a tap corroborates against its admitted snapshot after a same-lifetime record rebuild', async () => { + const store = makeSessionStore(); + const name = 'ios-admitted-tap-baseline'; + const ref = store.publish( + name, + makeIosSession(name, { + appBundleId: 'com.example.app', + snapshot: snapshot(imageViewerNodes), + }), + ); + store.update(ref, { snapshot: snapshot(profileNodes) }); + legacyDispatchCapture.mockImplementation(async (_device, command) => { + if (command === 'press') throw new AppError('XCTEST_RECORDED_FAILURE', 'tap failed'); + if (command === 'snapshot') return snapshotPayload(imageViewerNodes); + return {}; + }); + const response = await handleInteractionCommands({ + req: { token: 'test', session: name, command: 'click', positionals: ['104', '222'], flags: {} }, + sessionName: name, + sessionRef: ref, + sessionStore: store, + contextFromFlags, + ...getRuntimeBindings(), + }); + expect(response?.ok).toBe(true); + if (!response?.ok) throw new Error('landed tap should be corroborated'); + expect(response.data?.warning).toMatch(/same-scope post-action accessibility capture changed/); +}); diff --git a/src/daemon/interaction/internal/interaction-ios-tap-outcome.ts b/src/daemon/interaction/internal/interaction-ios-tap-outcome.ts index 977114c8d6..5fd0566d49 100644 --- a/src/daemon/interaction/internal/interaction-ios-tap-outcome.ts +++ b/src/daemon/interaction/internal/interaction-ios-tap-outcome.ts @@ -11,7 +11,7 @@ import { getRequestSignal } from '@agent-device/host-kit/request'; import { isLocalIosRunnerSession } from '../../direct-ios-selector.ts'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import type { SessionStore } from '../../session-store.ts'; -import type { SessionRef } from '../../session-state.ts'; +import type { SessionRef, SessionState } from '../../session-state.ts'; import type { BoundContextFromFlags, CaptureSnapshotForSession } from './types.ts'; const XCTEST_RECORDED_FAILURE = 'XCTEST_RECORDED_FAILURE'; @@ -54,8 +54,9 @@ export type IosTapCorroboration = { export async function corroborateIosTapFailure( params: IosTapCorroborationParams, ): Promise { - if (!canCorroborateIosTapFailure(params)) return undefined; - const baseline = readCorroborationBaseline(params.ref.session.snapshot); + const session = params.sessionStore.resolveCurrent(params.ref); + if (!session || !canCorroborateIosTapFailure(params, session)) return undefined; + const baseline = readCorroborationBaseline(session.snapshot); if (!baseline) return undefined; const after = await captureCorroborationSnapshot( @@ -69,11 +70,14 @@ export async function corroborateIosTapFailure( return compareCorroborationEvidence(baseline.snapshot, after, params.command); } -function canCorroborateIosTapFailure(params: IosTapCorroborationParams): boolean { +function canCorroborateIosTapFailure( + params: IosTapCorroborationParams, + session: SessionState, +): boolean { return ( isTapCommand(params.command) && asAppError(params.error).code === XCTEST_RECORDED_FAILURE && - isLocalIosRunnerSession(params.ref.session, { skipPendingPostGestureStabilization: false }) + isLocalIosRunnerSession(session, { skipPendingPostGestureStabilization: false }) ); } diff --git a/src/daemon/interaction/internal/interaction-touch-reference-frame.ts b/src/daemon/interaction/internal/interaction-touch-reference-frame.ts index ecf4a54b22..4edc7a8c38 100644 --- a/src/daemon/interaction/internal/interaction-touch-reference-frame.ts +++ b/src/daemon/interaction/internal/interaction-touch-reference-frame.ts @@ -72,7 +72,9 @@ export async function resolveDirectTouchReferenceFrameSafely(params: { level: 'warn', phase: 'touch_reference_frame_resolve_failed', data: { - platform: params.ref.session.device.platform, + platform: + params.sessionStore.resolveCurrent(params.ref)?.device.platform ?? + params.ref.session.device.platform, error: error instanceof Error ? error.message : String(error), }, }); diff --git a/src/daemon/selector-capture-runtime.ts b/src/daemon/selector-capture-runtime.ts index 2cfd92c54d..508ffca20b 100644 --- a/src/daemon/selector-capture-runtime.ts +++ b/src/daemon/selector-capture-runtime.ts @@ -197,6 +197,7 @@ async function runCapture( snapshotScope: string | undefined, interactiveOnly = request.flags?.snapshotInteractiveOnly, ): Promise { + const session = params.ref ? params.sessionStore.requireCurrent(params.ref) : undefined; const flags = { ...request.flags, snapshotInteractiveOnly: interactiveOnly, @@ -204,7 +205,7 @@ async function runCapture( const boundCapture = params.capture; const capture = await captureSnapshot({ device: params.device, - session: params.session, + session, flags, outPath: request.outPath ?? params.req.flags?.out, logPath: params.logPath ?? '', @@ -217,7 +218,7 @@ async function runCapture( flags, logPath: params.logPath ?? '', meta: params.req.meta, - session: params.session, + session, snapshotScope, includeRects: request.includeRects, // The POLL's remaining budget, not the request's. A binding's signal is fixed at diff --git a/src/daemon/session-snapshot.ts b/src/daemon/session-snapshot.ts index cfb26a848f..96efd49f5b 100644 --- a/src/daemon/session-snapshot.ts +++ b/src/daemon/session-snapshot.ts @@ -130,7 +130,8 @@ export function issueSettleRefs( settle: SettleObservation | undefined, ): number | undefined { if (!ref || !settle?.diff) return undefined; - const session = sessionStore.requireCurrent(ref); + const session = sessionStore.resolveCurrent(ref); + if (!session) return undefined; markSessionPartialRefsIssued(session, collectSettleIssuedRefBodies(settle)); return session.snapshotGeneration; } diff --git a/src/daemon/snapshot-command-runtime.ts b/src/daemon/snapshot-command-runtime.ts index 9cc847c758..6ec78b4bfb 100644 --- a/src/daemon/snapshot-command-runtime.ts +++ b/src/daemon/snapshot-command-runtime.ts @@ -146,15 +146,15 @@ function createSnapshotRuntime(params: { isRefScopedSnapshot(req), ); const snapshot = keepCurrentSnapshot ? current.snapshot : snapshotRecord.snapshot; - const nextSession: SessionState = - current ?? - createSnapshotSession({ - sessionName, - sessionScope: resolveSessionScope(req), - device, - snapshot, - appBundleId: record.appBundleId, - }); + const nextSession: SessionState = ref + ? sessionStore.update(ref, {}) + : createSnapshotSession({ + sessionName, + sessionScope: resolveSessionScope(req), + device, + snapshot, + appBundleId: record.appBundleId, + }); nextSession.appName = record.appName ?? current?.appName; setCommandSnapshot(nextSession, { snapshot, From 9403b974fc52f3b0814f55127762015b15f22ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 3 Oct 2026 11:10:36 +0200 Subject: [PATCH 2/2] fix: omit retired settle refs after the touch frame probe --- .../interaction-touch-response.test.ts | 82 +++++++++++++++++++ .../internal/interaction-touch-response.ts | 7 +- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts index a471a83106..c72da0e3eb 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts @@ -4,6 +4,7 @@ import { makeSessionStore } from '../../../../__tests__/test-utils/store-factory import { handleInteractionCommands } from '../../index.ts'; import { buildInteractionResponseData, + buildTargetedTouchResponsePayloads, transformTouchResponseData, } from '../interaction-touch-response.ts'; import type { PressCommandResult } from '@agent-device/contracts/interaction'; @@ -100,6 +101,87 @@ function selectorResult(readiness?: { polls: number; waitedMs: number }): PressC const WAITED_SELECTOR_RESULT = selectorResult({ polls: 3, waitedMs: 400 }); +test.each([false, true])( + 'point-touch publication after a held frame probe, retired=%s', + async (retired) => { + const sessionStore = makeSessionStore(); + const address = 'cwd:held-touch-response:default'; + const session = makeSession('default'); + session.snapshot = { + nodes: attachRefs([{ index: 0, type: 'Button', label: 'Continue' }]), + createdAt: Date.now(), + backend: 'xctest', + }; + session.snapshotGeneration = 3; + installTestScreenRecording(session); + const ref = sessionStore.publish(address, session); + let startProbe!: () => void; + let releaseProbe!: () => void; + const probing = new Promise((resolve) => { + startProbe = resolve; + }); + const released = new Promise((resolve) => { + releaseProbe = resolve; + }); + const result: PressCommandResult = { + kind: 'point', + point: { x: 10, y: 20 }, + settle: { + settled: true, + waitedMs: 25, + captures: 2, + quietMs: 25, + timeoutMs: 2000, + diff: { + summary: { additions: 1, removals: 0, unchanged: 0 }, + lines: [{ kind: 'added', text: 'Continue', ref: 'e1' }], + }, + }, + }; + const running = buildTargetedTouchResponsePayloads({ + params: { + req: { + token: 't', + command: 'press', + positionals: ['10', '20'], + session: 'default', + flags: {}, + }, + sessionName: address, + sessionRef: ref, + sessionStore, + contextFromFlags, + captureSnapshotForSession: async () => { + startProbe(); + await released; + return sessionStore.requireCurrent(ref).snapshot!; + }, + }, + session, + result, + staleRefsWarning: undefined, + extra: {}, + }); + await probing; + let successor; + if (retired) { + sessionStore.retire(ref); + successor = sessionStore.publish(address, makeSession('default')); + } + releaseProbe(); + const payloads = await running; + expect(payloads.responseData.x).toBe(10); + if (retired) { + expect(payloads.responseData.settle).toBeUndefined(); + expect(payloads.result.settle).toBeUndefined(); + expect(sessionStore.lookup(address)).toEqual(successor); + expect(successor?.session.snapshot).toBeUndefined(); + } else { + expect(payloads.responseData.settle).toMatchObject({ refsGeneration: 3, settled: true }); + } + }, +); + test('the response builder reports the resolved wait and keeps the prior warning', () => { const { responseData, result } = buildInteractionResponseData({ source: { kind: 'runtime', result: WAITED_SELECTOR_RESULT }, diff --git a/src/daemon/interaction/internal/interaction-touch-response.ts b/src/daemon/interaction/internal/interaction-touch-response.ts index e7235e216e..c13bce6f62 100644 --- a/src/daemon/interaction/internal/interaction-touch-response.ts +++ b/src/daemon/interaction/internal/interaction-touch-response.ts @@ -309,15 +309,18 @@ export async function buildTargetedTouchResponsePayloads(params: { handlerParams.sessionStore.requireCurrent(handlerParams.sessionRef!).snapshot?.nodes ?? [], ); + const currentResult = handlerParams.sessionStore.resolveCurrent(handlerParams.sessionRef!) + ? result + : { ...result, settle: undefined }; return buildInteractionResponseData({ - source: { kind: 'runtime', result, publicData }, + source: { kind: 'runtime', result: currentResult, publicData }, referenceFrame, extra, staleRefsWarning: params.staleRefsWarning, settleRefsGeneration: issueSettleRefs( handlerParams.sessionRef, handlerParams.sessionStore, - result.settle, + currentResult.settle, ), }); }