fix(voice): refuse unrequested Gemini tool calls and share pointer turns

in push-to-talk, answer a Gemini tool call that arrives with no input, reply
or tool result outstanding with a failure instead of running it, and refuse
every other call in the reply it starts, including one after unprompted
speech; the reply stays tracked so the next input retires it and its late
boundary cannot close the user's turn

in push-to-talk, a caption that arrives after its turn ended no longer opens
a reply, so the next request is not retired with its tool calls discarded
open mic cannot tell either case from a new reply and stays unchanged

check packet ownership after the narration callback before queuing tools,
and raise the per-connection call history to 4096 IDs

move the pointer turn event, context item and announcement into
pointerContext so the OpenAI and Gemini controllers share them, and drop the
Gemini token route's copy of the shared exact-Origin comparison
This commit is contained in:
Sameh Khamis committed 2026-10-19 12:30:54 -07:00
1 parent bd2342dc81
commit 1c727842df
7 files changed
+448 -91

No files matched your search

+67
View File
@@ -6,8 +6,13 @@ import {
POINTER_HOLD_MS,
buildPointerSnapshot,
createPointerTracker,
POINTER_NONE_KEY,
formatPointerContext,
pointerAnnouncement,
pointerChip,
pointerContextItem,
pointerTurnContext,
pointerTurnEvent,
} from './pointerContext.js';
function harness({ pick } = {}) {
@@ -227,3 +232,65 @@ test('chip: "this" names an entity, "here" gives the ground coordinate', () => {
assert.deepEqual(pointerChip(ground), { kind: 'here', label: '30.27, -97.74' });
assert.equal(pointerChip(null), null);
});
test('pointer turn helpers reveal gesture targets, describe implicit ones by kind and dedupe repeats', () => {
const snapshot = {
fresh: true,
at: 'keydown',
entity: { kind: 'aircraft', layerId: 'flights', label: 'UAL1' },
lat: 10,
lon: 20,
screen: { x: 5, y: 6 },
screenPx: { x: 5, y: 6 },
};
const shown = pointerTurnEvent(snapshot, true);
assert.equal(shown.announced, snapshot);
assert.deepEqual(shown.pointer, {
kind: 'this',
label: 'UAL1',
screenPx: { x: 5, y: 6 },
});
assert.deepEqual(pointerTurnEvent(snapshot, false), {
announced: null,
pointer: null,
});
const gesture = pointerTurnContext(snapshot, {
at: 'keydown',
gesture: true,
sentKey: null,
});
assert.deepEqual(gesture.item, pointerContextItem(snapshot));
assert.equal(
pointerTurnContext(snapshot, {
at: 'keydown',
gesture: true,
sentKey: gesture.key,
}),
null,
);
assert.deepEqual(
pointerTurnContext(snapshot, { at: 'text', gesture: false, sentKey: null })
.item,
{ type: 'pointer_context', at: 'text', pointing: 'aircraft' },
);
const cleared = pointerTurnContext(null, {
at: 'text',
gesture: false,
sentKey: gesture.key,
});
assert.deepEqual(cleared, {
item: { type: 'pointer_context', at: 'text', target: 'none' },
key: POINTER_NONE_KEY,
});
for (const sentKey of [null, POINTER_NONE_KEY])
assert.equal(
pointerTurnContext(null, { at: 'text', gesture: false, sentKey }),
null,
);
assert.deepEqual(pointerAnnouncement(snapshot, null), shown.pointer);
assert.equal(pointerAnnouncement(snapshot, snapshot), null);
assert.equal(pointerAnnouncement(null, null), null);
});