diff --git a/src/voice/gevActions.js b/src/voice/gevActions.js index 04be469c7..ddd35b127 100644 --- a/src/voice/gevActions.js +++ b/src/voice/gevActions.js @@ -1554,10 +1554,15 @@ export function createGevActionRunner({ conversation++; analystEngine?.reset(); }; - /** Voice was removed: nothing may run or write again. */ - runVoiceAction.dispose = () => { + /** + * Voice was removed, or this runner's globe left (`keepMemory`: a host + * that shares the analyst's memory with the next runner keeps the last + * answer): nothing may run or write again. + */ + runVoiceAction.dispose = ({ keepMemory = false } = {}) => { if (disposed) return; - runVoiceAction.resetConversation(); + conversation++; + if (!keepMemory) analystEngine?.reset(); disposed = true; }; return runVoiceAction; diff --git a/src/voice/integrationContract.test.mjs b/src/voice/integrationContract.test.mjs index faff3423a..f6ba2bc59 100644 --- a/src/voice/integrationContract.test.mjs +++ b/src/voice/integrationContract.test.mjs @@ -352,3 +352,35 @@ test('the cloud prompt states precedence and caveat speech once', () => { /"approximately"\/"roughly" for a tool number/, ); }); + +test('an engine switch keeps the last answer for a follow-up on the next engine', async () => { + const { createSceneRunner } = await import('./sceneRunner.js'); + const analystMemory = { lastResult: null }; + const layerEnabledAt = new Map(); + const run = createSceneRunner({ + build: () => + harness({ runnerOptions: { analystMemory, layerEnabledAt } }).runner, + }); + run.attach({ engine: { kind: 'cesium' } }); + const first = await run('analyst_query', { + layers: ['flights'], + scope: { kind: 'anywhere' }, + }); + assert.equal(first.ok, true); + // The real runner of the outgoing engine is disposed by the switch. + run.detach(); + run.attach({ engine: { kind: 'navara' } }); + const followUp = await run('analyst_query', { + layers: ['flights'], + followUp: true, + }); + assert.equal(followUp.ok, true, JSON.stringify(followUp)); + assert.equal(followUp.engine, 'navara'); + // The session ending still forgets it. + run.resetConversation(); + const afterReset = await run('analyst_query', { + layers: ['flights'], + followUp: true, + }); + assert.equal(afterReset.code, 'NO_RESULT_CONTEXT'); +}); diff --git a/src/voice/sceneRunner.js b/src/voice/sceneRunner.js index 9f2d7fd95..cb9db8d69 100644 --- a/src/voice/sceneRunner.js +++ b/src/voice/sceneRunner.js @@ -139,7 +139,9 @@ export function createSceneRunner({ const previous = inner; inner = null; kind = null; - previous?.dispose?.(); + // The conversation (the analyst's last answer) continues on the next + // engine: only this runner ends. + previous?.dispose?.({ keepMemory: true }); }; /** The page's switch settled: waiting calls look again. */ run.wake = wake; diff --git a/src/voice/voiceWorkflowHarness.mjs b/src/voice/voiceWorkflowHarness.mjs index f89f98411..7b0c5c2d7 100644 --- a/src/voice/voiceWorkflowHarness.mjs +++ b/src/voice/voiceWorkflowHarness.mjs @@ -40,6 +40,7 @@ export function harness({ flights = null, pointer = null, on = ['flights'], + runnerOptions = {}, } = {}) { const modules = { flights: flights || { @@ -97,6 +98,7 @@ export function harness({ return true; }, }, + ...runnerOptions, }); return { runner,