teamshare-bridge 0.26.19 → 0.26.21

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.
Files changed (46) hide show
  1. package/dist/bridge/daemon.js +60 -0
  2. package/dist/bridge/daemon.js.map +1 -1
  3. package/dist/bridge/local-server.js +29 -11
  4. package/dist/bridge/local-server.js.map +1 -1
  5. package/dist/bridge/session-relay.d.ts +14 -0
  6. package/dist/bridge/session-relay.js +53 -2
  7. package/dist/bridge/session-relay.js.map +1 -1
  8. package/dist/cli/commands/research.d.ts +7 -3
  9. package/dist/cli/commands/research.js +67 -15
  10. package/dist/cli/commands/research.js.map +1 -1
  11. package/dist/cli/commands/usage.js +85 -32
  12. package/dist/cli/commands/usage.js.map +1 -1
  13. package/dist/cli/index.js +1 -1
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/lib/brief.js +3 -2
  16. package/dist/lib/brief.js.map +1 -1
  17. package/dist/lib/browser/batch.d.ts +23 -0
  18. package/dist/lib/browser/batch.js +33 -0
  19. package/dist/lib/browser/batch.js.map +1 -1
  20. package/dist/lib/browser/manager.d.ts +1 -0
  21. package/dist/lib/browser/manager.js +1 -0
  22. package/dist/lib/browser/manager.js.map +1 -1
  23. package/dist/lib/browser/network-log.d.ts +28 -3
  24. package/dist/lib/browser/network-log.js +74 -2
  25. package/dist/lib/browser/network-log.js.map +1 -1
  26. package/dist/lib/browser/profile-sweep.d.ts +15 -0
  27. package/dist/lib/browser/profile-sweep.js +69 -0
  28. package/dist/lib/browser/profile-sweep.js.map +1 -0
  29. package/dist/lib/browser/screen-change.d.ts +5 -0
  30. package/dist/lib/browser/screen-change.js +22 -1
  31. package/dist/lib/browser/screen-change.js.map +1 -1
  32. package/dist/lib/browser/session-pool.d.ts +40 -0
  33. package/dist/lib/browser/session-pool.js +62 -1
  34. package/dist/lib/browser/session-pool.js.map +1 -1
  35. package/dist/lib/browser/session.d.ts +51 -8
  36. package/dist/lib/browser/session.js +144 -15
  37. package/dist/lib/browser/session.js.map +1 -1
  38. package/dist/lib/llm/usage-reader.d.ts +91 -4
  39. package/dist/lib/llm/usage-reader.js +750 -111
  40. package/dist/lib/llm/usage-reader.js.map +1 -1
  41. package/dist/lib/llm/usage.d.ts +9 -9
  42. package/dist/lib/llm/usage.js +9 -8
  43. package/dist/lib/llm/usage.js.map +1 -1
  44. package/dist/lib/orchestrator/handlers/browser.js +342 -56
  45. package/dist/lib/orchestrator/handlers/browser.js.map +1 -1
  46. package/package.json +1 -1
@@ -36,6 +36,26 @@ async function screenOf(session) {
36
36
  return null;
37
37
  }
38
38
  }
39
+ /**
40
+ * Pre-action settle context (2026-11): the current URL + timestamp, captured
41
+ * BEFORE a navigation-capable action runs. The post-action settle compares
42
+ * against these so an SPA route change or full load is visible. Best-effort
43
+ * and cheap (one pageInfo); a failure just means `fromUrl` is absent.
44
+ */
45
+ async function settleContext(session) {
46
+ const since = Date.now();
47
+ if (!session?.pageInfo)
48
+ return { since };
49
+ try {
50
+ const info = await session.pageInfo();
51
+ if (info?.url)
52
+ return { since, fromUrl: info.url };
53
+ }
54
+ catch {
55
+ /* best-effort */
56
+ }
57
+ return { since };
58
+ }
39
59
  /**
40
60
  * Emit an action result, attaching an inline screenshot when the session is in
41
61
  * QA vision mode (2026-10) AND a bounded per-action network delta (2026-10).
@@ -52,8 +72,34 @@ async function screenOf(session) {
52
72
  * payloads get one too: the toast or validation error that explains a failure
53
73
  * is exactly what the agent needs to see. The capture is BEST-EFFORT - a
54
74
  * screenshot problem must never turn a successful action into a failed one.
75
+ *
76
+ * 2026-11: in qaVision mode the screen is read AFTER a settle
77
+ * (`waitForScreenSettle`): the observer's mutation score, SPA route changes
78
+ * and full navigations are awaited first, so the screen + screenshot show the
79
+ * page the action produced instead of the mid-transition DOM. Pass
80
+ * `settle: false` to skip (e.g. a deliberate double-click sequence).
55
81
  */
56
- async function emitWithVision(emit, session, cmdId, payload) {
82
+ async function emitWithVision(emit, session, cmdId, payload, opts) {
83
+ const wantSettle = session?.connected === true &&
84
+ session?.qaVision === true &&
85
+ opts?.settle !== false &&
86
+ typeof session?.waitForScreenSettle === 'function';
87
+ const settled = wantSettle && session
88
+ ? await session.waitForScreenSettle({
89
+ since: opts?.since ?? Date.now(),
90
+ fromUrl: opts?.fromUrl,
91
+ }).catch(() => null)
92
+ : null;
93
+ if (settled && opts?.rescreen) {
94
+ try {
95
+ const fresh = await opts.rescreen();
96
+ if (fresh)
97
+ payload.screen = fresh;
98
+ }
99
+ catch {
100
+ /* keep the pre-settle screen */
101
+ }
102
+ }
57
103
  if (session?.connected && session.qaVision === true) {
58
104
  try {
59
105
  const shot = await session.screenshot('jpeg', 62);
@@ -66,6 +112,9 @@ async function emitWithVision(emit, session, cmdId, payload) {
66
112
  /* vision is best-effort */
67
113
  }
68
114
  }
115
+ if (settled && (settled.changed || settled.navigated)) {
116
+ payload.change = settled;
117
+ }
69
118
  if (session?.connected && typeof session.networkDelta === 'function') {
70
119
  try {
71
120
  const delta = await session.networkDelta(Date.now() - 8000, 10);
@@ -79,14 +128,30 @@ async function emitWithVision(emit, session, cmdId, payload) {
79
128
  emit('orchestrator:browser-result', cmdId, payload);
80
129
  }
81
130
  /**
82
- * The session an identity used most recently (2026-10).
131
+ * Sessions an identity opened, most recent last (2026-10, multi-window 2026-11).
83
132
  *
84
133
  * An agent that opened a NAMED session and then calls browser_action without a
85
- * sessionId must keep driving that same browser, not silently jump back to its
86
- * default one. Cleared when the session is closed.
134
+ * sessionId keeps driving that same browser. With TWO or more live windows and
135
+ * no sessionId the command is AMBIGUOUS and refused — acting on "whichever was
136
+ * touched last" would drive the wrong Chrome. Pruned when sessions close.
87
137
  */
88
- const lastSessionByAgent = new Map();
89
- /** The identity key used by `lastSessionByAgent` and the fallback session. */
138
+ const sessionsByAgent = new Map();
139
+ function rememberSession(identityKey, sessionId) {
140
+ const list = sessionsByAgent.get(identityKey) ?? [];
141
+ const next = [...list.filter((id) => id !== sessionId), sessionId];
142
+ sessionsByAgent.set(identityKey, next.slice(-20));
143
+ }
144
+ function forgetSession(identityKey, sessionId) {
145
+ const list = sessionsByAgent.get(identityKey);
146
+ if (!list)
147
+ return;
148
+ const next = list.filter((id) => id !== sessionId);
149
+ if (next.length)
150
+ sessionsByAgent.set(identityKey, next);
151
+ else
152
+ sessionsByAgent.delete(identityKey);
153
+ }
154
+ /** The identity key used by `sessionsByAgent` and the fallback session. */
90
155
  function identityKeyOf(agentUserId) {
91
156
  const normalized = (0, session_pool_1.normalizeSessionKey)(agentUserId);
92
157
  return normalized ? `agent:${normalized}` : 'shared';
@@ -109,19 +174,49 @@ async function handleBrowserCommand(data, emit) {
109
174
  });
110
175
  return;
111
176
  }
112
- // SESSION PER IDENTITY (2026-10, multi-browser): the old fixed 'default'
113
- // made every agent on this PC share one Chrome. The key is now the explicit
114
- // sessionId, else the caller's identity, with the last named session
115
- // remembered so later actions stay on the browser they opened.
177
+ // SESSION PER IDENTITY (2026-10, multi-window 2026-11): the old fixed
178
+ // 'default' made every agent on this PC share one Chrome. The key is the
179
+ // explicit sessionId, else the caller's remembered windows. With two or more
180
+ // live windows and no sessionId the command is AMBIGUOUS and refused.
116
181
  const identityKey = identityKeyOf(agentUserId);
117
182
  const explicitSession = (0, session_pool_1.normalizeSessionKey)(data.sessionId);
118
- const sessionId = explicitSession ??
119
- (action === 'connect' ? undefined : lastSessionByAgent.get(identityKey)) ??
120
- (0, session_pool_1.deriveSessionKey)(null, agentUserId);
183
+ const manager = (0, manager_1.getBrowserManager)();
184
+ const liveWindows = manager.list().map((id) => {
185
+ const s = manager.get(id);
186
+ return { id, actor: s?.actor ?? null };
187
+ });
188
+ const remembered = (sessionsByAgent.get(identityKey) ?? []).filter((id) => manager.get(id));
189
+ let sessionId;
190
+ if (explicitSession) {
191
+ sessionId = explicitSession;
192
+ }
193
+ else if (action === 'connect') {
194
+ sessionId = (0, session_pool_1.deriveSessionKey)(null, agentUserId);
195
+ }
196
+ else {
197
+ const resolved = (0, session_pool_1.resolveSessionForCommand)({
198
+ explicit: null,
199
+ remembered,
200
+ derived: (0, session_pool_1.deriveSessionKey)(null, agentUserId),
201
+ live: liveWindows,
202
+ });
203
+ if ('ambiguous' in resolved) {
204
+ const list = resolved.ambiguous
205
+ .map((w) => `${w.id}${w.actor ? ` (${w.actor})` : ''}`)
206
+ .join(', ');
207
+ emit('orchestrator:browser-result', String(data.cmdId ?? ''), {
208
+ success: false,
209
+ error: `BROWSER_SESSION_AMBIGUOUS: this agent has ${resolved.ambiguous.length} browser windows open ` +
210
+ `(${list}). Pass sessionId on every browser_action — each test account has its own window.`,
211
+ sessions: resolved.ambiguous,
212
+ });
213
+ return;
214
+ }
215
+ sessionId = resolved.sessionId;
216
+ }
121
217
  if (explicitSession || action === 'connect') {
122
- lastSessionByAgent.set(identityKey, sessionId);
218
+ rememberSession(identityKey, sessionId);
123
219
  }
124
- const manager = (0, manager_1.getBrowserManager)();
125
220
  // Any command counts as activity, so the idle-close grace restarts.
126
221
  manager.touch(sessionId);
127
222
  log.info(`Browser command: ${action} (session: ${sessionId})`);
@@ -167,6 +262,29 @@ async function handleBrowserCommand(data, emit) {
167
262
  // OPT-IN QA vision mode (default false, 2026-10): every mutating action
168
263
  // returns a screenshot. Only the QA brief turns it on.
169
264
  const qaVision = data.qaVision === true;
265
+ // One window per identity (2026-11): the caller names the account this
266
+ // window belongs to, and isolated forces a per-session profile so a QA
267
+ // window never inherits the human's shared debug profile.
268
+ const callActor = typeof data.actor === 'string' && data.actor.trim()
269
+ ? data.actor.trim().slice(0, 80)
270
+ : null;
271
+ const isolated = data.isolated === true;
272
+ // A tagged window never changes hands.
273
+ const tagged = manager.get(sessionId);
274
+ if (tagged?.actor && callActor) {
275
+ const gate = (0, session_pool_1.assertActorForLogin)({
276
+ sessionActor: tagged.actor,
277
+ callActor,
278
+ });
279
+ if (gate) {
280
+ emit('orchestrator:browser-result', data.cmdId, {
281
+ success: false,
282
+ error: gate.message,
283
+ code: gate.code,
284
+ });
285
+ break;
286
+ }
287
+ }
170
288
  // REUSE only a LIVE session. A disconnected one may have lost its port
171
289
  // and its profile to a concurrent browser while it was down, so it is
172
290
  // re-allocated below instead of silently reconnecting onto them.
@@ -174,6 +292,7 @@ async function handleBrowserCommand(data, emit) {
174
292
  if (existing?.connected) {
175
293
  existing.setAllowCredentials(allowCredentials);
176
294
  existing.setQaVision(qaVision);
295
+ existing.setActor(callActor);
177
296
  // reset:true drops a crashed run's leftovers (open modal, corrupted
178
297
  // field values, stale validation errors) before the new run starts.
179
298
  // A resumed run omits it and keeps its page.
@@ -193,6 +312,7 @@ async function handleBrowserCommand(data, emit) {
193
312
  alreadyConnected: true,
194
313
  sessionId,
195
314
  port: existing.port,
315
+ ...(existing.actor ? { actor: existing.actor } : {}),
196
316
  qaVision: existing.qaVision,
197
317
  ...pageState,
198
318
  ...(screen ? { screen } : {}),
@@ -218,6 +338,7 @@ async function handleBrowserCommand(data, emit) {
218
338
  sessionKey: sessionId,
219
339
  usedDirs: manager.usedProfileDirs(sessionId),
220
340
  ...(namedProfileDir ? { namedDir: namedProfileDir } : {}),
341
+ ...(isolated ? { forceIsolated: true } : {}),
221
342
  });
222
343
  if (profile.conflict) {
223
344
  emit('orchestrator:browser-result', data.cmdId, {
@@ -236,11 +357,13 @@ async function handleBrowserCommand(data, emit) {
236
357
  profileDir: profile.dir,
237
358
  allowCredentials,
238
359
  qaVision,
360
+ ...(callActor ? { actor: callActor } : {}),
239
361
  });
240
362
  // A reused session keeps its flag from a previous connect; honour the
241
363
  // explicit value on every connect so the caller stays in control.
242
364
  session.setAllowCredentials(allowCredentials);
243
365
  session.setQaVision(qaVision);
366
+ session.setActor(callActor);
244
367
  const info = await session.connect(port);
245
368
  const actualPort = session.port;
246
369
  log.info(`Browser session "${sessionId}" on port ${actualPort} ` +
@@ -254,6 +377,7 @@ async function handleBrowserCommand(data, emit) {
254
377
  sessionId,
255
378
  port: actualPort,
256
379
  profile: profile.shared ? 'shared' : 'isolated',
380
+ ...(callActor ? { actor: callActor } : {}),
257
381
  qaVision: session.qaVision,
258
382
  ...pageState,
259
383
  ...(screen ? { screen } : {}),
@@ -265,7 +389,8 @@ async function handleBrowserCommand(data, emit) {
265
389
  if (!session?.connected) {
266
390
  emit('orchestrator:browser-result', data.cmdId, {
267
391
  success: false,
268
- error: 'No active browser session. Call browser_open first.',
392
+ error: `No active browser session "${sessionId}". ` +
393
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
269
394
  });
270
395
  return;
271
396
  }
@@ -284,7 +409,8 @@ async function handleBrowserCommand(data, emit) {
284
409
  if (!session?.connected) {
285
410
  emit('orchestrator:browser-result', data.cmdId, {
286
411
  success: false,
287
- error: 'No active browser session.',
412
+ error: `No active browser session "${sessionId}". ` +
413
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
288
414
  });
289
415
  return;
290
416
  }
@@ -296,12 +422,24 @@ async function handleBrowserCommand(data, emit) {
296
422
  });
297
423
  return;
298
424
  }
425
+ // Capture the pre-navigation URL + time BEFORE navigating: the settle
426
+ // compares against these, so it can see an SPA route change and a full
427
+ // load alike.
428
+ const navFrom = await session
429
+ .pageInfo()
430
+ .then((info) => info.url ?? null)
431
+ .catch(() => null);
432
+ const navSince = Date.now();
299
433
  const info = await session.navigate(url);
300
434
  const screen = await screenOf(session);
301
435
  await emitWithVision(emit, session, data.cmdId, {
302
436
  success: true,
303
437
  ...info,
304
438
  ...(screen ? { screen } : {}),
439
+ }, {
440
+ since: navSince,
441
+ ...(navFrom ? { fromUrl: navFrom } : {}),
442
+ rescreen: () => screenOf(session),
305
443
  });
306
444
  break;
307
445
  }
@@ -310,7 +448,8 @@ async function handleBrowserCommand(data, emit) {
310
448
  if (!session?.connected) {
311
449
  emit('orchestrator:browser-result', data.cmdId, {
312
450
  success: false,
313
- error: 'No active browser session.',
451
+ error: `No active browser session "${sessionId}". ` +
452
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
314
453
  });
315
454
  return;
316
455
  }
@@ -393,6 +532,7 @@ async function handleBrowserCommand(data, emit) {
393
532
  }
394
533
  clicked = { x, y };
395
534
  }
535
+ const settleFrom = await settleContext(session);
396
536
  const screen = await screenOf(session);
397
537
  await emitWithVision(emit, session, data.cmdId, {
398
538
  success: true,
@@ -401,6 +541,9 @@ async function handleBrowserCommand(data, emit) {
401
541
  x: clicked.x,
402
542
  y: clicked.y,
403
543
  ...(screen ? { screen } : {}),
544
+ }, {
545
+ ...settleFrom,
546
+ rescreen: () => screenOf(session),
404
547
  });
405
548
  break;
406
549
  }
@@ -409,7 +552,8 @@ async function handleBrowserCommand(data, emit) {
409
552
  if (!session?.connected) {
410
553
  emit('orchestrator:browser-result', data.cmdId, {
411
554
  success: false,
412
- error: 'No active browser session.',
555
+ error: `No active browser session "${sessionId}". ` +
556
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
413
557
  });
414
558
  return;
415
559
  }
@@ -448,6 +592,7 @@ async function handleBrowserCommand(data, emit) {
448
592
  const observed = await session.type(text, {
449
593
  clearFirst: data.clearFirst === true,
450
594
  });
595
+ const settleFrom = await settleContext(session);
451
596
  const screen = await screenOf(session);
452
597
  await emitWithVision(emit, session, data.cmdId, {
453
598
  success: true,
@@ -455,6 +600,9 @@ async function handleBrowserCommand(data, emit) {
455
600
  length: text.length,
456
601
  observed,
457
602
  ...(screen ? { screen } : {}),
603
+ }, {
604
+ ...settleFrom,
605
+ rescreen: () => screenOf(session),
458
606
  });
459
607
  }
460
608
  catch (err) {
@@ -472,10 +620,30 @@ async function handleBrowserCommand(data, emit) {
472
620
  if (!session?.connected) {
473
621
  emit('orchestrator:browser-result', data.cmdId, {
474
622
  success: false,
475
- error: 'No active browser session. Call browser_open first.',
623
+ error: `No active browser session "${sessionId}". ` +
624
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
625
+ });
626
+ return;
627
+ }
628
+ // One window per identity (2026-11): a tagged window only signs in
629
+ // its own account. Never sign in as B in A's window.
630
+ const callActor = typeof data.actor === 'string' && data.actor.trim()
631
+ ? data.actor.trim()
632
+ : null;
633
+ const gate = (0, session_pool_1.assertActorForLogin)({
634
+ sessionActor: session.actor,
635
+ callActor,
636
+ });
637
+ if (gate) {
638
+ emit('orchestrator:browser-result', data.cmdId, {
639
+ success: false,
640
+ error: gate.message,
641
+ code: gate.code,
476
642
  });
477
643
  return;
478
644
  }
645
+ if (callActor)
646
+ session.setActor(callActor);
479
647
  if (data.allowCredentials === true)
480
648
  session.setAllowCredentials(true);
481
649
  // Password is optional: an `email_otp` / OTP-only step sends the email
@@ -495,11 +663,15 @@ async function handleBrowserCommand(data, emit) {
495
663
  : {}),
496
664
  });
497
665
  // Never echo the password: the payload carries booleans + an error code.
666
+ const settleFrom = await settleContext(session);
498
667
  const screen = await screenOf(session);
499
668
  await emitWithVision(emit, session, data.cmdId, {
500
669
  success: true,
501
670
  ...result,
502
671
  ...(screen ? { screen } : {}),
672
+ }, {
673
+ ...settleFrom,
674
+ rescreen: () => screenOf(session),
503
675
  });
504
676
  break;
505
677
  }
@@ -508,7 +680,8 @@ async function handleBrowserCommand(data, emit) {
508
680
  if (!session?.connected) {
509
681
  emit('orchestrator:browser-result', data.cmdId, {
510
682
  success: false,
511
- error: 'No active browser session. Call browser_open first.',
683
+ error: `No active browser session "${sessionId}". ` +
684
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
512
685
  });
513
686
  return;
514
687
  }
@@ -534,12 +707,21 @@ async function handleBrowserCommand(data, emit) {
534
707
  : {}),
535
708
  ...(data.clearFirst === false ? {} : {}),
536
709
  });
710
+ // An OTP submit navigates (2026-11): settle before reading, like
711
+ // every other navigation-capable action, or the screen shows the
712
+ // pre-submit form while the app is already on the next page.
713
+ const settleFrom = await settleContext(session);
537
714
  const screen = await screenOf(session);
538
715
  await emitWithVision(emit, session, data.cmdId, {
539
716
  success: result.ok,
540
- ...(result.ok ? {} : { error: result.error ?? 'OTP_FILL_FAILED' }),
717
+ ...(result.ok
718
+ ? {}
719
+ : { error: result.error ?? 'OTP_FILL_FAILED' }),
541
720
  ...result,
542
721
  ...(screen ? { screen } : {}),
722
+ }, {
723
+ ...settleFrom,
724
+ rescreen: () => screenOf(session),
543
725
  });
544
726
  }
545
727
  catch (err) {
@@ -557,17 +739,22 @@ async function handleBrowserCommand(data, emit) {
557
739
  if (!session?.connected) {
558
740
  emit('orchestrator:browser-result', data.cmdId, {
559
741
  success: false,
560
- error: 'No active browser session.',
742
+ error: `No active browser session "${sessionId}". ` +
743
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
561
744
  });
562
745
  return;
563
746
  }
564
747
  const key = String(data.key ?? '');
748
+ const settleFrom = await settleContext(session);
565
749
  await session.press(key);
566
750
  const screen = await screenOf(session);
567
751
  await emitWithVision(emit, session, data.cmdId, {
568
752
  success: true,
569
753
  pressed: key,
570
754
  ...(screen ? { screen } : {}),
755
+ }, {
756
+ ...settleFrom,
757
+ rescreen: () => screenOf(session),
571
758
  });
572
759
  break;
573
760
  }
@@ -579,7 +766,8 @@ async function handleBrowserCommand(data, emit) {
579
766
  if (!session?.connected) {
580
767
  emit('orchestrator:browser-result', data.cmdId, {
581
768
  success: false,
582
- error: 'No active browser session.',
769
+ error: `No active browser session "${sessionId}". ` +
770
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
583
771
  });
584
772
  return;
585
773
  }
@@ -633,7 +821,8 @@ async function handleBrowserCommand(data, emit) {
633
821
  if (!session?.connected) {
634
822
  emit('orchestrator:browser-result', data.cmdId, {
635
823
  success: false,
636
- error: 'No active browser session.',
824
+ error: `No active browser session "${sessionId}". ` +
825
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
637
826
  });
638
827
  return;
639
828
  }
@@ -689,7 +878,8 @@ async function handleBrowserCommand(data, emit) {
689
878
  if (!session?.connected) {
690
879
  emit('orchestrator:browser-result', data.cmdId, {
691
880
  success: false,
692
- error: 'No active browser session.',
881
+ error: `No active browser session "${sessionId}". ` +
882
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
693
883
  });
694
884
  return;
695
885
  }
@@ -704,16 +894,19 @@ async function handleBrowserCommand(data, emit) {
704
894
  }
705
895
  // 2026-10: the interactive-element ref list (`e1 … eN`), including
706
896
  // portalled menus/popovers/dialogs. Actions accept `ref`.
897
+ // 2026-11: `filter` narrows by substring over role + name + tag
898
+ // (`getRefs {filter:"button"}`); numbering is always full-scan.
707
899
  case 'getRefs': {
708
900
  const session = manager.get(sessionId);
709
901
  if (!session?.connected) {
710
902
  emit('orchestrator:browser-result', data.cmdId, {
711
903
  success: false,
712
- error: 'No active browser session. Call browser_open first.',
904
+ error: `No active browser session "${sessionId}". ` +
905
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
713
906
  });
714
907
  return;
715
908
  }
716
- const refs = await session.getRefs();
909
+ const refs = await session.getRefs(typeof data.filter === 'string' ? data.filter : undefined);
717
910
  let oneTimeCode;
718
911
  try {
719
912
  const snap = await session.snapshot({ refs: false });
@@ -741,7 +934,8 @@ async function handleBrowserCommand(data, emit) {
741
934
  if (!session?.connected) {
742
935
  emit('orchestrator:browser-result', data.cmdId, {
743
936
  success: false,
744
- error: 'No active browser session. Call browser_open first.',
937
+ error: `No active browser session "${sessionId}". ` +
938
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
745
939
  });
746
940
  return;
747
941
  }
@@ -760,7 +954,8 @@ async function handleBrowserCommand(data, emit) {
760
954
  if (!session?.connected) {
761
955
  emit('orchestrator:browser-result', data.cmdId, {
762
956
  success: false,
763
- error: 'No active browser session.',
957
+ error: `No active browser session "${sessionId}". ` +
958
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
764
959
  });
765
960
  return;
766
961
  }
@@ -773,12 +968,65 @@ async function handleBrowserCommand(data, emit) {
773
968
  });
774
969
  break;
775
970
  }
971
+ // 2026-11: set the viewport before a design-verification screenshot.
972
+ // The design's viewport IS the contract; the backend clamps to
973
+ // 320–8192 and the read-back reports what the page actually uses.
974
+ case 'setViewport': {
975
+ const session = manager.get(sessionId);
976
+ if (!session?.connected) {
977
+ emit('orchestrator:browser-result', data.cmdId, {
978
+ success: false,
979
+ error: `No active browser session "${sessionId}". ` +
980
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
981
+ });
982
+ return;
983
+ }
984
+ const width = typeof data.viewportWidth === 'number'
985
+ ? data.viewportWidth
986
+ : typeof data.width === 'number'
987
+ ? data.width
988
+ : NaN;
989
+ const height = typeof data.viewportHeight === 'number'
990
+ ? data.viewportHeight
991
+ : typeof data.height === 'number'
992
+ ? data.height
993
+ : NaN;
994
+ if (!Number.isFinite(width) || !Number.isFinite(height)) {
995
+ emit('orchestrator:browser-result', data.cmdId, {
996
+ success: false,
997
+ error: 'setViewport needs viewportWidth + viewportHeight (320-8192).',
998
+ });
999
+ return;
1000
+ }
1001
+ try {
1002
+ const applied = await session.setViewport(width, height, typeof data.deviceScaleFactor === 'number'
1003
+ ? data.deviceScaleFactor
1004
+ : 1);
1005
+ const screen = await screenOf(session);
1006
+ await emitWithVision(emit, session, data.cmdId, {
1007
+ success: true,
1008
+ viewport: true,
1009
+ ...applied,
1010
+ ...(screen ? { screen } : {}),
1011
+ });
1012
+ }
1013
+ catch (err) {
1014
+ const screen = await screenOf(session);
1015
+ await emitWithVision(emit, session, data.cmdId, {
1016
+ success: false,
1017
+ error: err instanceof Error ? err.message : String(err),
1018
+ ...(screen ? { screen } : {}),
1019
+ });
1020
+ }
1021
+ break;
1022
+ }
776
1023
  case 'evaluate': {
777
1024
  const session = manager.get(sessionId);
778
1025
  if (!session?.connected) {
779
1026
  emit('orchestrator:browser-result', data.cmdId, {
780
1027
  success: false,
781
- error: 'No active browser session.',
1028
+ error: `No active browser session "${sessionId}". ` +
1029
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
782
1030
  });
783
1031
  return;
784
1032
  }
@@ -795,7 +1043,8 @@ async function handleBrowserCommand(data, emit) {
795
1043
  if (!session?.connected) {
796
1044
  emit('orchestrator:browser-result', data.cmdId, {
797
1045
  success: false,
798
- error: 'No active browser session.',
1046
+ error: `No active browser session "${sessionId}". ` +
1047
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
799
1048
  });
800
1049
  return;
801
1050
  }
@@ -812,7 +1061,8 @@ async function handleBrowserCommand(data, emit) {
812
1061
  if (!session?.connected) {
813
1062
  emit('orchestrator:browser-result', data.cmdId, {
814
1063
  success: false,
815
- error: 'No active browser session.',
1064
+ error: `No active browser session "${sessionId}". ` +
1065
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
816
1066
  });
817
1067
  return;
818
1068
  }
@@ -831,7 +1081,8 @@ async function handleBrowserCommand(data, emit) {
831
1081
  if (!session?.connected) {
832
1082
  emit('orchestrator:browser-result', data.cmdId, {
833
1083
  success: false,
834
- error: 'No active browser session. Call browser_open first.',
1084
+ error: `No active browser session "${sessionId}". ` +
1085
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
835
1086
  });
836
1087
  return;
837
1088
  }
@@ -871,7 +1122,8 @@ async function handleBrowserCommand(data, emit) {
871
1122
  if (!session?.connected) {
872
1123
  emit('orchestrator:browser-result', data.cmdId, {
873
1124
  success: false,
874
- error: 'No active browser session. Call browser_open first.',
1125
+ error: `No active browser session "${sessionId}". ` +
1126
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
875
1127
  });
876
1128
  return;
877
1129
  }
@@ -929,7 +1181,8 @@ async function handleBrowserCommand(data, emit) {
929
1181
  if (!session?.connected) {
930
1182
  emit('orchestrator:browser-result', data.cmdId, {
931
1183
  success: false,
932
- error: 'No active browser session. Call browser_open first.',
1184
+ error: `No active browser session "${sessionId}". ` +
1185
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
933
1186
  });
934
1187
  return;
935
1188
  }
@@ -951,7 +1204,8 @@ async function handleBrowserCommand(data, emit) {
951
1204
  if (!session?.connected) {
952
1205
  emit('orchestrator:browser-result', data.cmdId, {
953
1206
  success: false,
954
- error: 'No active browser session. Call browser_open first.',
1207
+ error: `No active browser session "${sessionId}". ` +
1208
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
955
1209
  });
956
1210
  return;
957
1211
  }
@@ -961,11 +1215,15 @@ async function handleBrowserCommand(data, emit) {
961
1215
  ? { promptText: data.promptText }
962
1216
  : {}),
963
1217
  });
1218
+ const settleFrom = await settleContext(session);
964
1219
  const screen = await screenOf(session);
965
1220
  await emitWithVision(emit, session, data.cmdId, {
966
1221
  success: result.ok,
967
1222
  ...result,
968
1223
  ...(screen ? { screen } : {}),
1224
+ }, {
1225
+ ...settleFrom,
1226
+ rescreen: () => screenOf(session),
969
1227
  });
970
1228
  break;
971
1229
  }
@@ -978,7 +1236,8 @@ async function handleBrowserCommand(data, emit) {
978
1236
  if (!session?.connected) {
979
1237
  emit('orchestrator:browser-result', data.cmdId, {
980
1238
  success: false,
981
- error: 'No active browser session. Call browser_open first.',
1239
+ error: `No active browser session "${sessionId}". ` +
1240
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
982
1241
  });
983
1242
  return;
984
1243
  }
@@ -1003,7 +1262,8 @@ async function handleBrowserCommand(data, emit) {
1003
1262
  if (!session?.connected) {
1004
1263
  emit('orchestrator:browser-result', data.cmdId, {
1005
1264
  success: false,
1006
- error: 'No active browser session. Call browser_open first.',
1265
+ error: `No active browser session "${sessionId}". ` +
1266
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1007
1267
  });
1008
1268
  return;
1009
1269
  }
@@ -1028,7 +1288,8 @@ async function handleBrowserCommand(data, emit) {
1028
1288
  if (!session?.connected) {
1029
1289
  emit('orchestrator:browser-result', data.cmdId, {
1030
1290
  success: false,
1031
- error: 'No active browser session. Call browser_open first.',
1291
+ error: `No active browser session "${sessionId}". ` +
1292
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1032
1293
  });
1033
1294
  return;
1034
1295
  }
@@ -1053,7 +1314,8 @@ async function handleBrowserCommand(data, emit) {
1053
1314
  if (!session?.connected) {
1054
1315
  emit('orchestrator:browser-result', data.cmdId, {
1055
1316
  success: false,
1056
- error: 'No active browser session. Call browser_open first.',
1317
+ error: `No active browser session "${sessionId}". ` +
1318
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1057
1319
  });
1058
1320
  return;
1059
1321
  }
@@ -1074,7 +1336,8 @@ async function handleBrowserCommand(data, emit) {
1074
1336
  if (!session?.connected) {
1075
1337
  emit('orchestrator:browser-result', data.cmdId, {
1076
1338
  success: false,
1077
- error: 'No active browser session. Call browser_open first.',
1339
+ error: `No active browser session "${sessionId}". ` +
1340
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1078
1341
  });
1079
1342
  return;
1080
1343
  }
@@ -1106,7 +1369,8 @@ async function handleBrowserCommand(data, emit) {
1106
1369
  if (!session?.connected) {
1107
1370
  emit('orchestrator:browser-result', data.cmdId, {
1108
1371
  success: false,
1109
- error: 'No active browser session.',
1372
+ error: `No active browser session "${sessionId}". ` +
1373
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1110
1374
  });
1111
1375
  return;
1112
1376
  }
@@ -1123,7 +1387,8 @@ async function handleBrowserCommand(data, emit) {
1123
1387
  if (!session?.connected) {
1124
1388
  emit('orchestrator:browser-result', data.cmdId, {
1125
1389
  success: false,
1126
- error: 'No active browser session.',
1390
+ error: `No active browser session "${sessionId}". ` +
1391
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1127
1392
  });
1128
1393
  return;
1129
1394
  }
@@ -1140,7 +1405,8 @@ async function handleBrowserCommand(data, emit) {
1140
1405
  if (!session?.connected) {
1141
1406
  emit('orchestrator:browser-result', data.cmdId, {
1142
1407
  success: false,
1143
- error: 'No active browser session.',
1408
+ error: `No active browser session "${sessionId}". ` +
1409
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1144
1410
  });
1145
1411
  return;
1146
1412
  }
@@ -1161,7 +1427,8 @@ async function handleBrowserCommand(data, emit) {
1161
1427
  if (!session?.connected) {
1162
1428
  emit('orchestrator:browser-result', data.cmdId, {
1163
1429
  success: false,
1164
- error: 'No active browser session.',
1430
+ error: `No active browser session "${sessionId}". ` +
1431
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1165
1432
  });
1166
1433
  return;
1167
1434
  }
@@ -1204,7 +1471,8 @@ async function handleBrowserCommand(data, emit) {
1204
1471
  if (!session?.connected) {
1205
1472
  emit('orchestrator:browser-result', data.cmdId, {
1206
1473
  success: false,
1207
- error: 'No active browser session.',
1474
+ error: `No active browser session "${sessionId}". ` +
1475
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1208
1476
  });
1209
1477
  return;
1210
1478
  }
@@ -1245,7 +1513,8 @@ async function handleBrowserCommand(data, emit) {
1245
1513
  if (!session?.connected) {
1246
1514
  emit('orchestrator:browser-result', data.cmdId, {
1247
1515
  success: false,
1248
- error: 'No active browser session.',
1516
+ error: `No active browser session "${sessionId}". ` +
1517
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1249
1518
  });
1250
1519
  return;
1251
1520
  }
@@ -1281,7 +1550,8 @@ async function handleBrowserCommand(data, emit) {
1281
1550
  if (!session?.connected) {
1282
1551
  emit('orchestrator:browser-result', data.cmdId, {
1283
1552
  success: false,
1284
- error: 'No active browser session.',
1553
+ error: `No active browser session "${sessionId}". ` +
1554
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1285
1555
  });
1286
1556
  return;
1287
1557
  }
@@ -1314,7 +1584,8 @@ async function handleBrowserCommand(data, emit) {
1314
1584
  if (!session?.connected) {
1315
1585
  emit('orchestrator:browser-result', data.cmdId, {
1316
1586
  success: false,
1317
- error: 'No active browser session.',
1587
+ error: `No active browser session "${sessionId}". ` +
1588
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1318
1589
  });
1319
1590
  return;
1320
1591
  }
@@ -1354,17 +1625,22 @@ async function handleBrowserCommand(data, emit) {
1354
1625
  if (!session?.connected) {
1355
1626
  emit('orchestrator:browser-result', data.cmdId, {
1356
1627
  success: false,
1357
- error: 'No active browser session.',
1628
+ error: `No active browser session "${sessionId}". ` +
1629
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1358
1630
  });
1359
1631
  return;
1360
1632
  }
1361
1633
  const result = await session.submit(typeof data.selector === 'string' ? data.selector : undefined);
1634
+ const settleFrom = await settleContext(session);
1362
1635
  await emitWithVision(emit, session, data.cmdId, {
1363
1636
  success: result?.ok === true,
1364
1637
  submitted: result?.ok === true,
1365
1638
  ...(result?.ok
1366
1639
  ? {}
1367
1640
  : { error: result?.error ?? 'the form could not be submitted' }),
1641
+ }, {
1642
+ ...settleFrom,
1643
+ rescreen: () => screenOf(session),
1368
1644
  });
1369
1645
  break;
1370
1646
  }
@@ -1377,18 +1653,30 @@ async function handleBrowserCommand(data, emit) {
1377
1653
  if (!session?.connected) {
1378
1654
  emit('orchestrator:browser-result', data.cmdId, {
1379
1655
  success: false,
1380
- error: 'No active browser session.',
1656
+ error: `No active browser session "${sessionId}". ` +
1657
+ `Call browser_open({ sessionId: "${sessionId}" }) first.`,
1381
1658
  });
1382
1659
  return;
1383
1660
  }
1384
1661
  const steps = data.actions;
1385
- const summary = await (0, batch_1.executeBatch)(session, steps ?? []);
1662
+ // One window per identity (2026-11): the batch carries ONE session, so
1663
+ // a top-level `actor` applies to every step that does not name its own.
1664
+ // Without this, a batched login could not be attributed (or gated).
1665
+ const batchActor = typeof data.actor === 'string' && data.actor.trim()
1666
+ ? data.actor.trim()
1667
+ : null;
1668
+ const resolvedSteps = (steps ?? []).map((step) => batchActor && !step.actor ? { ...step, actor: batchActor } : step);
1669
+ const settleFrom = await settleContext(session);
1670
+ const summary = await (0, batch_1.executeBatch)(session, resolvedSteps);
1386
1671
  const screen = await screenOf(session);
1387
1672
  await emitWithVision(emit, session, data.cmdId, {
1388
1673
  success: true,
1389
1674
  batch: true,
1390
1675
  ...summary,
1391
1676
  ...(screen ? { screen } : {}),
1677
+ }, {
1678
+ ...settleFrom,
1679
+ rescreen: () => screenOf(session),
1392
1680
  });
1393
1681
  break;
1394
1682
  }
@@ -1403,9 +1691,7 @@ async function handleBrowserCommand(data, emit) {
1403
1691
  // never claims a termination that did not happen (the pre-check flag
1404
1692
  // could say `true` while the kill silently failed).
1405
1693
  const terminated = await manager.close(sessionId);
1406
- if (lastSessionByAgent.get(identityKey) === sessionId) {
1407
- lastSessionByAgent.delete(identityKey);
1408
- }
1694
+ forgetSession(identityKey, sessionId);
1409
1695
  emit('orchestrator:browser-result', data.cmdId, {
1410
1696
  success: true,
1411
1697
  closed: true,