livedesk 0.1.605 → 0.1.607

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -80,11 +80,12 @@ const RETIRED_CLIENT_MODEL_SINGLE_OPTIONS = new Set(['--ai', '--no-ai', '--fake-
80
80
  const RETIRED_CLIENT_MODEL_VALUE_OPTION = '--ai-model';
81
81
  let activeAgentProcess = null;
82
82
  let roleRestartRequest = null;
83
- let agentRestartRequest = null;
84
- let linuxVideoAccelerationStatus = null;
85
- let discoveryWakeController = new AbortController();
86
- let networkChangeMonitor = null;
87
- const sessionRefreshesInFlight = new WeakMap();
83
+ let agentRestartRequest = null;
84
+ let linuxVideoAccelerationStatus = null;
85
+ let discoveryWakeController = new AbortController();
86
+ let networkChangeMonitor = null;
87
+ let desktopMainAuthSession = null;
88
+ const sessionRefreshesInFlight = new WeakMap();
88
89
  const disposeAgentTerminationHandlers = installAgentTerminationHandlers({
89
90
  getAgentProcess: () => activeAgentProcess
90
91
  });
@@ -1097,8 +1098,11 @@ export function clearCachedHubTarget() {
1097
1098
  }
1098
1099
  }
1099
1100
 
1100
- function readSavedSessionFromFile() {
1101
- for (const path of [preferredClientAuthPath(), CLIENT_AUTH_PATH, UNIFIED_CLIENT_AUTH_PATH]) {
1101
+ function readSavedSessionFromFile() {
1102
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
1103
+ return desktopMainAuthSession;
1104
+ }
1105
+ for (const path of [preferredClientAuthPath(), CLIENT_AUTH_PATH, UNIFIED_CLIENT_AUTH_PATH]) {
1102
1106
  try {
1103
1107
  const state = JSON.parse(readFileSync(path, 'utf8'));
1104
1108
  const raw = state?.[CLIENT_AUTH_STORAGE_KEY];
@@ -1113,20 +1117,43 @@ function readSavedSessionFromFile() {
1113
1117
  return null;
1114
1118
  }
1115
1119
 
1116
- function writeSavedSessionToFile(session) {
1117
- const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
1118
- if (!normalized.ok) {
1119
- return false;
1120
- }
1121
- const storage = createFileStorage(preferredClientAuthPath());
1120
+ function writeSavedSessionToFile(session) {
1121
+ const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
1122
+ if (!normalized.ok) {
1123
+ return false;
1124
+ }
1125
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
1126
+ return Boolean(desktopMainAuthSession?.access_token);
1127
+ }
1128
+ const storage = createFileStorage(preferredClientAuthPath());
1122
1129
  storage.setItem(CLIENT_AUTH_STORAGE_KEY, JSON.stringify({ ...session, ...normalized.session }));
1123
1130
  return true;
1124
1131
  }
1125
1132
 
1126
- function clearSavedSession() {
1127
- rmSync(CLIENT_AUTH_PATH, { force: true });
1128
- rmSync(UNIFIED_CLIENT_AUTH_PATH, { force: true });
1129
- }
1133
+ function clearSavedSession() {
1134
+ desktopMainAuthSession = null;
1135
+ rmSync(CLIENT_AUTH_PATH, { force: true });
1136
+ rmSync(UNIFIED_CLIENT_AUTH_PATH, { force: true });
1137
+ }
1138
+
1139
+ export function acceptDesktopMainAuthSession(session) {
1140
+ if (!isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) return null;
1141
+ const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
1142
+ if (!normalized.ok) return null;
1143
+ desktopMainAuthSession = {
1144
+ ...session,
1145
+ ...normalized.session,
1146
+ user: {
1147
+ ...(session?.user && typeof session.user === 'object' ? session.user : {}),
1148
+ ...normalized.session.user
1149
+ }
1150
+ };
1151
+ return desktopMainAuthSession;
1152
+ }
1153
+
1154
+ export function clearDesktopMainAuthSession() {
1155
+ desktopMainAuthSession = null;
1156
+ }
1130
1157
 
1131
1158
  export async function fetchSupabaseWithDeadline(
1132
1159
  input,
@@ -1167,22 +1194,28 @@ export async function fetchSupabaseWithDeadline(
1167
1194
  }
1168
1195
  }
1169
1196
 
1170
- async function createSupabaseClient() {
1171
- const { createClient } = await import('@supabase/supabase-js');
1172
- return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
1173
- global: {
1174
- fetch: fetchSupabaseWithDeadline
1175
- },
1176
- auth: {
1177
- autoRefreshToken: false,
1178
- persistSession: true,
1179
- detectSessionInUrl: false,
1180
- flowType: 'pkce',
1181
- storageKey: CLIENT_AUTH_STORAGE_KEY,
1182
- storage: createFileStorage(preferredClientAuthPath())
1183
- }
1184
- });
1185
- }
1197
+ export async function createSupabaseClient() {
1198
+ const { createClient } = await import('@supabase/supabase-js');
1199
+ const desktopMainOwnsAuth = isTruthy(process.env.LIVEDESK_DESKTOP_HOST);
1200
+ return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
1201
+ global: {
1202
+ fetch: fetchSupabaseWithDeadline
1203
+ },
1204
+ ...(desktopMainOwnsAuth ? {
1205
+ // Electron Main rotates and encrypts the refresh token. The local
1206
+ // runtime receives only the current in-memory session and asks this
1207
+ // client to attach its access token to database requests.
1208
+ accessToken: async () => desktopMainAuthSession?.access_token || null
1209
+ } : { auth: {
1210
+ autoRefreshToken: false,
1211
+ persistSession: true,
1212
+ detectSessionInUrl: false,
1213
+ flowType: 'pkce',
1214
+ storageKey: CLIENT_AUTH_STORAGE_KEY,
1215
+ storage: createFileStorage(preferredClientAuthPath())
1216
+ } })
1217
+ });
1218
+ }
1186
1219
 
1187
1220
  function getNestedErrorCode(error) {
1188
1221
  let cursor = error;
@@ -1313,8 +1346,12 @@ async function recoverRotatedSession(supabase, previousSession) {
1313
1346
  return null;
1314
1347
  }
1315
1348
 
1316
- async function refreshSessionIfNeededCore(supabase) {
1317
- let existingSession = null;
1349
+ async function refreshSessionIfNeededCore(supabase) {
1350
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
1351
+ const normalized = normalizeRuntimeAuthSession(desktopMainAuthSession, { requireRefreshToken: true });
1352
+ return normalized.ok ? desktopMainAuthSession : null;
1353
+ }
1354
+ let existingSession = null;
1318
1355
  try {
1319
1356
  const { data: existing } = await supabase.auth.getSession();
1320
1357
  existingSession = existing?.session || null;
@@ -1382,12 +1419,17 @@ function openBrowser(url) {
1382
1419
  spawn(command, [launchUrl.toString()], { detached: true, stdio: 'ignore' }).unref();
1383
1420
  }
1384
1421
 
1385
- async function activateSupabaseSession(supabase, session) {
1386
- const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
1387
- if (!normalized.ok) {
1388
- throw new Error(normalized.error);
1389
- }
1390
- const { data: current } = await supabase.auth.getSession();
1422
+ async function activateSupabaseSession(supabase, session) {
1423
+ const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
1424
+ if (!normalized.ok) {
1425
+ throw new Error(normalized.error);
1426
+ }
1427
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
1428
+ const activeSession = acceptDesktopMainAuthSession(session);
1429
+ if (!activeSession) throw new Error('desktop-main-session-required');
1430
+ return activeSession;
1431
+ }
1432
+ const { data: current } = await supabase.auth.getSession();
1391
1433
  if (current?.session?.access_token === normalized.session.access_token) {
1392
1434
  const activeSession = current.session;
1393
1435
  if (!writeSavedSessionToFile(activeSession)) {
@@ -3173,7 +3215,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
3173
3215
 
3174
3216
  if (requestUrl.pathname === '/logout') {
3175
3217
  try {
3176
- await supabase.auth.signOut();
3218
+ await supabase.auth.signOut({ scope: 'local' });
3177
3219
  } catch {
3178
3220
  }
3179
3221
  clearSavedSession();
@@ -3410,10 +3452,13 @@ async function chooseClientConnection(supabase, options = {}) {
3410
3452
  roleVersion: process.env.LIVEDESK_ROLE_VERSION,
3411
3453
  savedSession: options.savedSession,
3412
3454
  loadSavedSession: options.loadSavedSession,
3413
- initialChoice: options.initialChoice,
3414
- initialChoiceMessage: options.initialChoiceMessage,
3415
- beginGoogleSignIn: async redirectTo => {
3416
- const activeSupabase = await getSupabase();
3455
+ initialChoice: options.initialChoice,
3456
+ initialChoiceMessage: options.initialChoiceMessage,
3457
+ onAuthSessionAccepted: acceptDesktopMainAuthSession,
3458
+ onAuthSessionCleared: clearDesktopMainAuthSession,
3459
+ beginGoogleSignIn: async redirectTo => {
3460
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) throw new Error('desktop-main-auth-required');
3461
+ const activeSupabase = await getSupabase();
3417
3462
  if (!activeSupabase?.auth) throw new Error('supabase-session-required');
3418
3463
  const { data, error } = await activeSupabase.auth.signInWithOAuth({
3419
3464
  provider: 'google',
@@ -3426,9 +3471,10 @@ async function chooseClientConnection(supabase, options = {}) {
3426
3471
  if (error) throw error;
3427
3472
  if (!data?.url) throw new Error('google-authorization-url-missing');
3428
3473
  return { url: data.url };
3429
- },
3430
- exchangeGoogleCode: async code => {
3431
- const activeSupabase = await getSupabase();
3474
+ },
3475
+ exchangeGoogleCode: async code => {
3476
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) throw new Error('desktop-main-auth-required');
3477
+ const activeSupabase = await getSupabase();
3432
3478
  if (!activeSupabase?.auth) throw new Error('supabase-session-required');
3433
3479
  const { data, error } = await activeSupabase.auth.exchangeCodeForSession(code);
3434
3480
  if (error) throw error;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.260",
3
+ "version": "0.1.262",
4
4
  "description": "VuvoDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,10 +42,10 @@
42
42
  "ws": "^8.18.3"
43
43
  },
44
44
  "optionalDependencies": {
45
- "@livedesk/fast-linux-x64": "0.1.457",
46
- "@livedesk/fast-osx-arm64": "0.1.457",
47
- "@livedesk/fast-osx-x64": "0.1.457",
48
- "@livedesk/fast-win-x64": "0.1.457"
45
+ "@livedesk/fast-linux-x64": "0.1.458",
46
+ "@livedesk/fast-osx-arm64": "0.1.458",
47
+ "@livedesk/fast-osx-x64": "0.1.458",
48
+ "@livedesk/fast-win-x64": "0.1.458"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"
@@ -979,9 +979,13 @@ function writeSavedSession(session) {
979
979
  return true;
980
980
  }
981
981
 
982
- function clearSavedSession() {
983
- writePrivateJsonAtomic(CLIENT_AUTH_PATH, {});
984
- }
982
+ function clearSavedSession() {
983
+ if (process.env.LIVEDESK_DESKTOP_HOST === '1') {
984
+ rmSync(CLIENT_AUTH_PATH, { force: true });
985
+ return;
986
+ }
987
+ writePrivateJsonAtomic(CLIENT_AUTH_PATH, {});
988
+ }
985
989
 
986
990
  function readBody(req) {
987
991
  return new Promise((resolveBody, reject) => {
@@ -1316,8 +1320,11 @@ export function createClientRuntimeServer(options = {}) {
1316
1320
  runtime.emit('client.auth.session', { result: state.auth.lastResult, error: message });
1317
1321
  };
1318
1322
 
1319
- const complete = (choice, message = 'Client credentials accepted. Finding the Hub.') => {
1320
- lastChoice = choice || lastChoice;
1323
+ const complete = (choice, message = 'Client credentials accepted. Finding the Hub.') => {
1324
+ if (choice?.session?.access_token) {
1325
+ options.onAuthSessionAccepted?.(choice.session);
1326
+ }
1327
+ lastChoice = choice || lastChoice;
1321
1328
  if (completed) {
1322
1329
  if (choice?.session?.access_token) {
1323
1330
  const accountProfile = readClientAccountProfile(choice.session);
@@ -1634,9 +1641,10 @@ export function createClientRuntimeServer(options = {}) {
1634
1641
  respondJson(200, { ok: true, authenticated: true, persisted: true, role: 'client' });
1635
1642
  return;
1636
1643
  }
1637
- if (pathname === '/api/auth/session' && req.method === 'DELETE') {
1638
- clearSavedSession();
1639
- loggedOut = true;
1644
+ if (pathname === '/api/auth/session' && req.method === 'DELETE') {
1645
+ clearSavedSession();
1646
+ options.onAuthSessionCleared?.();
1647
+ loggedOut = true;
1640
1648
  completed = false;
1641
1649
  lastChoice = null;
1642
1650
  state.manager = '';
@@ -130,7 +130,7 @@ if (!ffmpegStaticPath || !existsSync(ffmpegStaticPath)) {
130
130
  throw new Error('The packaged ffmpeg-static decoder is unavailable.');
131
131
  }
132
132
  let ffmpegPath = ffmpegStaticPath;
133
- try {
133
+ try {
134
134
  ffmpegPath = String(require('@ffmpeg-installer/ffmpeg')?.path || ffmpegStaticPath);
135
135
  } catch {
136
136
  // ffmpeg-static remains the portable decoder and compatibility fallback.
@@ -554,7 +554,7 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
554
554
  const socket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/input/ws`, {
555
555
  perMessageDeflate: false
556
556
  });
557
- const sendInput = async (input, timeoutMs = 5_000) => {
557
+ const sendInput = async (input, timeoutMs = 5_000) => {
558
558
  const outcome = waitForWebSocketJson(
559
559
  socket,
560
560
  message => (
@@ -577,17 +577,101 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
577
577
  captureGeneration: binding.captureGeneration
578
578
  }
579
579
  }));
580
- return outcome;
581
- };
582
-
583
- let resetApplied = false;
584
- try {
585
- const ready = waitForWebSocketJson(socket, message => message.type === 'RemoteInputSocketReady');
586
- await Promise.all([waitForWebSocketOpen(socket), ready]);
587
- const probe = await sendInput({
588
- type: 'keyboard.probe',
589
- inputSeq: 1,
590
- inputEventId: `windows-physical-probe-${Date.now()}`,
580
+ try {
581
+ return await outcome;
582
+ } catch (error) {
583
+ throw new Error(
584
+ `Windows input ${input.type} seq=${input.inputSeq} did not return an outcome: `
585
+ + `${error instanceof Error ? error.message : String(error)}`
586
+ );
587
+ }
588
+ };
589
+
590
+ const dispatchReset = (reason) => {
591
+ if (socket.readyState !== WebSocket.OPEN) {
592
+ return false;
593
+ }
594
+ socket.send(JSON.stringify({
595
+ type: 'input',
596
+ deviceId: binding.deviceId,
597
+ fireAndForget: true,
598
+ input: {
599
+ type: 'keyboard.reset',
600
+ inputSeq: 0,
601
+ requestAck: false,
602
+ pressedCodes: [],
603
+ shiftKey: false,
604
+ ctrlKey: false,
605
+ altKey: false,
606
+ metaKey: false,
607
+ repeat: false,
608
+ reason,
609
+ monitorIndex: binding.monitorIndex,
610
+ controlSessionId: binding.sessionId,
611
+ controlCommandId: binding.commandId,
612
+ captureGeneration: binding.captureGeneration
613
+ }
614
+ }));
615
+ return true;
616
+ };
617
+
618
+ let resetDispatched = false;
619
+ try {
620
+ const ready = waitForWebSocketJson(socket, message => message.type === 'RemoteInputSocketReady');
621
+ await Promise.all([waitForWebSocketOpen(socket), ready]);
622
+ const pointer = await sendInput({
623
+ type: 'pointermove',
624
+ inputSeq: 1,
625
+ inputEventId: `windows-physical-pointer-${Date.now()}`,
626
+ normalizedX: 0.5,
627
+ normalizedY: 0.5
628
+ });
629
+ assert.equal(pointer.type, 'RemoteInputApplied', String(pointer.error || 'Windows pointer input failed.'));
630
+ assert.equal(pointer.duplicate, false, 'Windows pointer input was acknowledged as a duplicate.');
631
+ assert.equal(pointer.nativeInputBackend, 'win32-setcursorpos');
632
+ assert.equal(Number(pointer.nativeEventsRequested), 1);
633
+ assert.equal(Number(pointer.nativeEventsAccepted), 1);
634
+
635
+ const shiftDown = await sendInput({
636
+ type: 'keydown',
637
+ inputSeq: 2,
638
+ inputEventId: `windows-physical-shift-down-${Date.now()}`,
639
+ key: 'Shift',
640
+ code: 'ShiftLeft',
641
+ keyCode: 16,
642
+ location: 1,
643
+ pressedCodes: ['ShiftLeft'],
644
+ shiftKey: true,
645
+ ctrlKey: false,
646
+ altKey: false,
647
+ metaKey: false,
648
+ repeat: false
649
+ });
650
+ assert.equal(shiftDown.type, 'RemoteInputApplied', String(shiftDown.error || 'Windows Shift keydown failed.'));
651
+ assert.equal(shiftDown.duplicate, false, 'Windows Shift keydown was acknowledged as a duplicate.');
652
+
653
+ const shiftUp = await sendInput({
654
+ type: 'keyup',
655
+ inputSeq: 3,
656
+ inputEventId: `windows-physical-shift-up-${Date.now()}`,
657
+ key: 'Shift',
658
+ code: 'ShiftLeft',
659
+ keyCode: 16,
660
+ location: 1,
661
+ pressedCodes: [],
662
+ shiftKey: false,
663
+ ctrlKey: false,
664
+ altKey: false,
665
+ metaKey: false,
666
+ repeat: false
667
+ });
668
+ assert.equal(shiftUp.type, 'RemoteInputApplied', String(shiftUp.error || 'Windows Shift keyup failed.'));
669
+ assert.equal(shiftUp.duplicate, false, 'Windows Shift keyup was acknowledged as a duplicate.');
670
+
671
+ const probe = await sendInput({
672
+ type: 'keyboard.probe',
673
+ inputSeq: 4,
674
+ inputEventId: `windows-physical-probe-${Date.now()}`,
591
675
  pressedCodes: [],
592
676
  shiftKey: false,
593
677
  ctrlKey: false,
@@ -606,26 +690,31 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
606
690
  /diagnostic keyboard isolation hook did not become ready/i,
607
691
  'The forced late hook start did not fail at the bounded ready deadline.'
608
692
  );
609
- const reset = await sendInput({
610
- type: 'keyboard.reset',
611
- inputSeq: 2,
612
- inputEventId: `windows-late-start-reset-${Date.now()}`,
613
- pressedCodes: [],
614
- shiftKey: false,
615
- ctrlKey: false,
616
- altKey: false,
617
- metaKey: false,
618
- repeat: false,
619
- reason: 'windows-hook-late-start-cleanup'
620
- });
621
- assert.equal(reset.type, 'RemoteInputApplied', String(reset.error || 'Windows late-start reset failed.'));
622
- assert.equal(reset.duplicate, false, 'Windows late-start reset was acknowledged as a duplicate.');
623
- resetApplied = true;
624
- return {
625
- lateStartFailure: true,
626
- error: String(probe.error || ''),
627
- resetApplied
628
- };
693
+ const recovery = await sendInput({
694
+ type: 'pointermove',
695
+ inputSeq: 5,
696
+ inputEventId: `windows-late-start-recovery-${Date.now()}`,
697
+ normalizedX: 0.5,
698
+ normalizedY: 0.5
699
+ });
700
+ assert.equal(
701
+ recovery.type,
702
+ 'RemoteInputApplied',
703
+ String(recovery.error || 'Windows input owner did not recover after the bounded hook failure.')
704
+ );
705
+ assert.equal(recovery.nativeInputBackend, 'win32-setcursorpos');
706
+ assert.equal(Number(recovery.nativeEventsRequested), 1);
707
+ assert.equal(Number(recovery.nativeEventsAccepted), 1);
708
+ resetDispatched = dispatchReset('windows-hook-late-start-cleanup');
709
+ assert.equal(resetDispatched, true, 'Windows late-start cleanup reset was not dispatched.');
710
+ return {
711
+ lateStartFailure: true,
712
+ pointerBackend: String(pointer.nativeInputBackend || ''),
713
+ normalKeyAcks: 2,
714
+ error: String(probe.error || ''),
715
+ recoveryApplied: true,
716
+ resetDispatched
717
+ };
629
718
  }
630
719
  assert.equal(probe.type, 'RemoteInputApplied', String(probe.error || 'Windows native input probe failed.'));
631
720
  assert.equal(probe.duplicate, false, 'Windows native input probe was acknowledged as a duplicate.');
@@ -637,57 +726,28 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
637
726
  assert.equal(probe.focusChanged, false, 'Windows native input probe changed the foreground window.');
638
727
  assert.equal(probe.inputDiagnostic, true);
639
728
 
640
- const reset = await sendInput({
641
- type: 'keyboard.reset',
642
- inputSeq: 2,
643
- inputEventId: `windows-physical-reset-${Date.now()}`,
644
- pressedCodes: [],
645
- shiftKey: false,
646
- ctrlKey: false,
647
- altKey: false,
648
- metaKey: false,
649
- repeat: false,
650
- reason: 'windows-native-input-diagnostic-cleanup'
651
- });
652
- assert.equal(reset.type, 'RemoteInputApplied', String(reset.error || 'Windows native input reset failed.'));
653
- assert.equal(reset.duplicate, false, 'Windows native input reset was acknowledged as a duplicate.');
654
- resetApplied = true;
655
- return {
656
- backend: String(probe.nativeInputBackend || ''),
729
+ resetDispatched = dispatchReset('windows-native-input-diagnostic-cleanup');
730
+ assert.equal(resetDispatched, true, 'Windows native input cleanup reset was not dispatched.');
731
+ return {
732
+ pointerBackend: String(pointer.nativeInputBackend || ''),
733
+ pointerRequested: Number(pointer.nativeEventsRequested || 0),
734
+ pointerAccepted: Number(pointer.nativeEventsAccepted || 0),
735
+ normalKeyAcks: 2,
736
+ backend: String(probe.nativeInputBackend || ''),
657
737
  requested: Number(probe.nativeEventsRequested || 0),
658
738
  accepted: Number(probe.nativeEventsAccepted || 0),
659
739
  hookObserved: Number(probe.hookEventsObserved || 0),
660
740
  hookBlocked: Number(probe.hookEventsBlocked || 0),
661
741
  duplicate: probe.duplicate === true,
662
742
  focusChanged: probe.focusChanged === true,
663
- resetApplied
664
- };
665
- } finally {
666
- if (!resetApplied && socket.readyState === WebSocket.OPEN) {
667
- try {
668
- socket.send(JSON.stringify({
669
- type: 'input',
670
- deviceId: binding.deviceId,
671
- fireAndForget: true,
672
- input: {
673
- type: 'keyboard.reset',
674
- inputSeq: 0,
675
- requestAck: false,
676
- pressedCodes: [],
677
- shiftKey: false,
678
- ctrlKey: false,
679
- altKey: false,
680
- metaKey: false,
681
- repeat: false,
682
- reason: 'windows-native-input-diagnostic-finally',
683
- monitorIndex: binding.monitorIndex,
684
- controlSessionId: binding.sessionId,
685
- controlCommandId: binding.commandId,
686
- captureGeneration: binding.captureGeneration
687
- }
688
- }));
689
- } catch {
690
- // Closing the exact input owner also triggers RemoteFast ReleaseAll.
743
+ resetDispatched
744
+ };
745
+ } finally {
746
+ if (!resetDispatched) {
747
+ try {
748
+ dispatchReset('windows-native-input-diagnostic-finally');
749
+ } catch {
750
+ // Closing the exact input owner also triggers RemoteFast ReleaseAll.
691
751
  }
692
752
  }
693
753
  try { socket.close(); } catch {}
@@ -1704,9 +1764,9 @@ function removeMacDiagnosticSignalHandlers() {
1704
1764
  macSignalHandlers.clear();
1705
1765
  }
1706
1766
 
1707
- installMacDiagnosticSignalHandlers();
1708
-
1709
- try {
1767
+ installMacDiagnosticSignalHandlers();
1768
+
1769
+ inputDiagnostic: try {
1710
1770
  macIsolation = await prepareMacDiagnosticIsolation();
1711
1771
  if (isMacPhysicalGate) {
1712
1772
  macDiagnosticAbort.throwIfRequested();
@@ -1924,7 +1984,8 @@ try {
1924
1984
  if (metadata.deviceId !== connectedDeviceId || metadata.frameMode !== 'mode3-h264-hw') return;
1925
1985
  const payload = Buffer.from(data.subarray(4 + metadataLength));
1926
1986
  frames.push({ metadata, receivedAt: performance.now(), bytes: payload.length, payload });
1927
- if (frames.length >= (isMacPhysicalGate ? 75 : 90)) {
1987
+ const requiredFrameCount = windowsInputAck ? 1 : (isMacPhysicalGate ? 75 : 90);
1988
+ if (frames.length >= requiredFrameCount) {
1928
1989
  finish(resolve, frames);
1929
1990
  }
1930
1991
  };
@@ -1953,9 +2014,40 @@ try {
1953
2014
  monitorSelections: { [connectedDeviceId]: requestedMonitorIndex }
1954
2015
  }));
1955
2016
 
1956
- const frames = await withMacDiagnosticAbort(framePromise, cancelFrameWait);
1957
- let activeInputBindingFrame = frames.at(-1);
1958
- const initialCaptureIdentity = switchMonitor && isWindows
2017
+ const frames = await withMacDiagnosticAbort(framePromise, cancelFrameWait);
2018
+ let activeInputBindingFrame = frames.at(-1);
2019
+ if (windowsInputAck) {
2020
+ const inputProbe = await runWindowsInputAckProbe({
2021
+ deviceId: connectedDeviceId,
2022
+ sessionId: String(activeInputBindingFrame.metadata.sessionId || ''),
2023
+ commandId: String(activeInputBindingFrame.metadata.commandId || ''),
2024
+ captureGeneration: Number(activeInputBindingFrame.metadata.captureGeneration || 0),
2025
+ monitorIndex: Number(activeInputBindingFrame.metadata.monitorIndex || 0)
2026
+ }, {
2027
+ expectHookLateStartFailure: windowsInputHookLateStart
2028
+ });
2029
+ if (windowsInputHookLateStart) {
2030
+ console.log(
2031
+ `Windows diagnostic hook late-start cleanup OK: expected timeout, `
2032
+ + `Agent responsive=${inputProbe.recoveryApplied}, reset dispatched=${inputProbe.resetDispatched}.`
2033
+ );
2034
+ } else {
2035
+ console.log(
2036
+ `Windows pointer input ACK: ${inputProbe.pointerAccepted}/${inputProbe.pointerRequested} `
2037
+ + `via ${inputProbe.pointerBackend}; normal key ACK: ${inputProbe.normalKeyAcks}/2; `
2038
+ + `isolated modifier ACK: `
2039
+ + `${inputProbe.accepted}/${inputProbe.requested} via ${inputProbe.backend}, `
2040
+ + `hook=${inputProbe.hookObserved}/${inputProbe.hookBlocked}, `
2041
+ + `duplicate=${inputProbe.duplicate}, `
2042
+ + `focusChanged=${inputProbe.focusChanged}, reset dispatched=${inputProbe.resetDispatched}.`
2043
+ );
2044
+ }
2045
+ // The Windows input gate shares only the minimum current Control binding
2046
+ // setup with the video smoke. Encoder profile and soak assertions belong
2047
+ // to their own commands and must not obscure native input evidence.
2048
+ break inputDiagnostic;
2049
+ }
2050
+ const initialCaptureIdentity = switchMonitor && isWindows
1959
2051
  ? await waitFor(
1960
2052
  () => findWindowsCaptureIdentity(agentLogs, requestedMonitorIndex),
1961
2053
  3_000,
@@ -2426,31 +2518,6 @@ try {
2426
2518
  console.log(`Mode 3 monitor transition OK: ${requestedMonitorIndex} -> ${nextMonitorIndex}, first frame ${switchedFirstFrameMs.toFixed(0)}ms, generation ${previousCaptureGeneration} -> ${firstSwitchedFrame.metadata.captureGeneration}.`);
2427
2519
  }
2428
2520
 
2429
- if (windowsInputAck) {
2430
- const inputProbe = await runWindowsInputAckProbe({
2431
- deviceId: connectedDeviceId,
2432
- sessionId: String(activeInputBindingFrame.metadata.sessionId || ''),
2433
- commandId: String(activeInputBindingFrame.metadata.commandId || ''),
2434
- captureGeneration: Number(activeInputBindingFrame.metadata.captureGeneration || 0),
2435
- monitorIndex: Number(activeInputBindingFrame.metadata.monitorIndex || 0)
2436
- }, {
2437
- expectHookLateStartFailure: windowsInputHookLateStart
2438
- });
2439
- if (windowsInputHookLateStart) {
2440
- console.log(
2441
- `Windows diagnostic hook late-start cleanup OK: expected timeout, `
2442
- + `Agent responsive, reset=${inputProbe.resetApplied}.`
2443
- );
2444
- } else {
2445
- console.log(
2446
- `Windows native modifier input ACK: ${inputProbe.accepted}/${inputProbe.requested} `
2447
- + `via ${inputProbe.backend}, hook=${inputProbe.hookObserved}/${inputProbe.hookBlocked}, `
2448
- + `duplicate=${inputProbe.duplicate}, `
2449
- + `focusChanged=${inputProbe.focusChanged}, reset=${inputProbe.resetApplied}.`
2450
- );
2451
- }
2452
- }
2453
-
2454
2521
  if (transitionToMode5) {
2455
2522
  const previousFrameSocket = frameSocket;
2456
2523
  frameSocket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/frames/ws?devices=${encodeURIComponent(connectedDeviceId)}`);