@inetafrica/open-claudia 3.5.23 → 3.5.25

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.
@@ -7,10 +7,21 @@
7
7
  // the browser. It lasts 120 s; the page's heartbeat and every accepted input extend it.
8
8
  // - one input WebSocket per agent-browser session (ws://127.0.0.1:<port>/?maxFps=2): events
9
9
  // are checked strictly, field by field, and forwarded in order. The same socket keeps the
10
- // latest frame, which Teach me saves as a click's picture.
10
+ // latest frame, which Teach me saves as a click's (or a navigation's) picture.
11
+ // - the address bar: go to an address, back, forward, reload. The stream socket only takes
12
+ // input, so these are agent-browser commands on the same session, run in the session's queue
13
+ // after the input before them, with the bot's own agent-browser environment and never a
14
+ // launch flag (core/agent-browser-cli.js: other launch settings would restart the browser).
15
+ // Only public internet addresses (core/browser-address.js).
16
+ // - the phone and web view (core/browser-drive.js): a take or a "fit" with the driver's screen
17
+ // makes the bot's tab take its size, phone layout and touch, and then the driver's input goes
18
+ // straight to Chrome through that view (typed text too: input_text). Only with Chrome in its
19
+ // own container; elsewhere the browser keeps its size and input goes through agent-browser.
11
20
  // - hand back (release, a closed panel, or a lock found expired by the 30 s sweeper): the lock
12
- // goes, the socket closes, any recording stops, and the bot is woken in the person's own chat
13
- // to carry on (C6). A recording stopped at the same time folds into one wake-up.
21
+ // goes, the view and the socket close, any recording stops, and the bot is woken in the
22
+ // person's own chat (C6), but only when it waits for them: it sent them an Open Browser card,
23
+ // or one of its browser commands was refused while they drove. A recording stopped at the
24
+ // same time folds into one wake-up. Otherwise the hand back is only logged.
14
25
  //
15
26
  // Every route answers { status, body }; web.js writes it. Agent Space reaches these routes with
16
27
  // the control token only.
@@ -19,6 +30,9 @@ const WebSocket = require("ws");
19
30
  const lockFile = require("./browser-lock");
20
31
  const view = require("./browser-view");
21
32
  const teach = require("./browser-teach");
33
+ const address = require("./browser-address");
34
+ const drive = require("./browser-drive");
35
+ const { runAgentBrowser } = require("./agent-browser-cli");
22
36
 
23
37
  const LOCK_TTL_MS = lockFile.LOCK_TTL_MS;
24
38
  // Inputs extend the lock; the file (fsynced) is rewritten at most this often, so the lock ends
@@ -31,19 +45,32 @@ const MAX_EVENTS = 50;
31
45
  const MAX_PENDING_EVENTS = 1000;
32
46
  const COORD_MAX = 10000;
33
47
  const ID_RE = /^[A-Za-z0-9._:-]{1,100}$/;
48
+ // An address-bar command still running after this is killed, and the person told it failed.
49
+ const NAVIGATE_TIMEOUT_MS = 20 * 1000;
50
+ const NAVIGATIONS = new Set(["open", "back", "forward", "reload"]);
51
+
52
+ // An Open Browser card counts as a request for a person this long.
53
+ const REQUEST_MAX_AGE_MS = 24 * 60 * 60 * 1000;
34
54
 
35
55
  const HAND_BACK_REASONS = {
36
56
  closed: " (they closed the Browser)",
37
57
  expired: " (their Browser went quiet for 2 minutes, so control came back on its own)",
38
58
  };
39
59
 
40
- let deps = {
41
- now: () => Date.now(),
42
- listSessions: () => view.listSessions(),
43
- connect: (url) => new WebSocket(url),
44
- scheduler: null, // core/scheduler by default (through core/cards.js wakeOrigin)
45
- runFor: null, // the chat's provider, settings and live session; activeRun() by default
46
- };
60
+ function defaultDeps() {
61
+ return {
62
+ now: () => Date.now(),
63
+ listSessions: () => view.listSessions(),
64
+ connect: (url) => new WebSocket(url),
65
+ scheduler: null, // core/scheduler by default (through core/cards.js wakeOrigin)
66
+ runFor: null, // the chat's provider, settings and live session; activeRun() by default
67
+ execFile: null, // child_process.execFile by default (core/agent-browser-cli.js)
68
+ lookupHost: null, // dns.promises.lookup by default (core/browser-address.js)
69
+ navigateTimeoutMs: NAVIGATE_TIMEOUT_MS,
70
+ };
71
+ }
72
+
73
+ let deps = defaultDeps();
47
74
 
48
75
  function reply(status, body) {
49
76
  return { status, body };
@@ -70,6 +97,19 @@ function sameUser(lock, who) {
70
97
  return !!lock && !!who && String(lock.userId) === String(who.userId);
71
98
  }
72
99
 
100
+ // The live lock this person holds on this session, extended (every accepted input or navigation
101
+ // extends it; the file is rewritten at most every LOCK_REWRITE_MS), or null.
102
+ function extendDriverLock(session, who) {
103
+ const now = deps.now();
104
+ const lock = lockFile.liveLock(now);
105
+ if (!lock || lock.session !== session || !sameUser(lock, who)) return null;
106
+ if (lockFile.lockExpiresAt(lock) - now < LOCK_TTL_MS - LOCK_REWRITE_MS) {
107
+ lock.expiresAt = iso(now + LOCK_TTL_MS);
108
+ lockFile.writeLock(lock);
109
+ }
110
+ return lock;
111
+ }
112
+
73
113
  // ---------------------------------------------------------------------------
74
114
  // C3: input events, checked field by field. Only the listed fields are forwarded.
75
115
 
@@ -77,6 +117,7 @@ const MOUSE_EVENTS = new Set(["mousePressed", "mouseReleased", "mouseMoved", "mo
77
117
  const MOUSE_BUTTONS = new Set(["none", "left", "middle", "right"]);
78
118
  const KEY_EVENTS = new Set(["keyDown", "keyUp", "char"]);
79
119
  const TOUCH_EVENTS = new Set(["touchStart", "touchMove", "touchEnd", "touchCancel"]);
120
+ const TEXT_MAX = drive.TEXT_MAX;
80
121
 
81
122
  const isNumber = (v) => typeof v === "number" && Number.isFinite(v);
82
123
  const inRange = (v, lo, hi) => isNumber(v) && v >= lo && v <= hi;
@@ -133,11 +174,34 @@ function checkEvent(ev) {
133
174
  }
134
175
  const out = { type: "input_touch", eventType: ev.eventType, touchPoints };
135
176
  if (!optional(out, "modifiers", modifiers)) return null;
177
+ // When the finger moved, on the driver's device (ms): keeps a swipe's speed (core/browser-drive.js).
178
+ if (!optional(out, "t", (v) => inRange(v, 0, 1e13))) return null;
136
179
  return out;
137
180
  }
181
+ // Text the driver typed (their phone keyboard, a paste), inserted where the page's caret is.
182
+ if (ev.type === "input_text") {
183
+ if (typeof ev.text !== "string" || !ev.text || Array.from(ev.text).length > TEXT_MAX) return null;
184
+ return { type: "input_text", text: ev.text };
185
+ }
138
186
  return null;
139
187
  }
140
188
 
189
+ // input_text as key presses, for a bot without the driver's view (agent-browser's socket takes
190
+ // no text) and for Teach me, which counts typing from key presses.
191
+ function textAsKeys(text) {
192
+ const out = [];
193
+ for (const ch of Array.from(text)) {
194
+ if (ch === "\n" || ch === "\r") {
195
+ out.push({ type: "input_keyboard", eventType: "keyDown", key: "Enter", code: "Enter", text: "\r", windowsVirtualKeyCode: 13 });
196
+ out.push({ type: "input_keyboard", eventType: "keyUp", key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 });
197
+ } else {
198
+ out.push({ type: "input_keyboard", eventType: "keyDown", key: ch, text: ch });
199
+ out.push({ type: "input_keyboard", eventType: "keyUp", key: ch });
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+
141
205
  // ---------------------------------------------------------------------------
142
206
  // Input sockets: one per agent-browser session, opened on take (or on the first input after a
143
207
  // restart), closed on hand back.
@@ -295,7 +359,23 @@ function wakeChat({ chatId, userId }, prompt, label) {
295
359
 
296
360
  function handBackLine(name, reason) {
297
361
  const who = teach.cleanName(name) || "Someone";
298
- return `[Browser] ${who} handed the browser back${HAND_BACK_REASONS[reason] || ""}. If you were waiting on them, check the page first (it may have changed) and carry on; otherwise just note it.`;
362
+ return `[Browser] ${who} handed the browser back${HAND_BACK_REASONS[reason] || ""}. Check the page first (it may have changed), then carry on with what you were waiting on them for.`;
363
+ }
364
+
365
+ // Was the bot waiting for this person when they handed back? It asked for them with an Open
366
+ // Browser card in their chat (since the day before they took over), or one of its own browser
367
+ // commands was refused while they drove.
368
+ function botWasWaiting(lock) {
369
+ let asked = [];
370
+ try {
371
+ const since = Date.parse(lock.since);
372
+ asked = require("./cards").takeBrowserRequests(lock.chatId, (Number.isFinite(since) ? since : deps.now()) - REQUEST_MAX_AGE_MS, deps.now());
373
+ } catch (e) {
374
+ console.error(`[browser] reading the bot's Open Browser cards failed: ${e.message}`);
375
+ }
376
+ const refused = lockFile.waitedDuring(lock);
377
+ lockFile.clearWaiting();
378
+ return asked.length > 0 || refused;
299
379
  }
300
380
 
301
381
  // ---------------------------------------------------------------------------
@@ -305,7 +385,39 @@ function lockBody(lock) {
305
385
  return { driving: true, driver: { name: lock.name } };
306
386
  }
307
387
 
308
- async function take({ session, who } = {}) {
388
+ // The driver's view on this lock's session: start it (or move it to a new screen) and remember it
389
+ // in the lock, so a restarted bot can bring it back. -> the fit, or null (the browser keeps its size).
390
+ async function startView(lock, viewport) {
391
+ if (!viewport || !drive.supported()) return null;
392
+ const info = sessionInfo(lock.session);
393
+ if (!info) return null;
394
+ const r = drive.active(lock.session)
395
+ ? await drive.fit(viewport)
396
+ : await drive.start({ session: info, viewport, savedBounds: lock.bounds || null });
397
+ const now = lockFile.readLock();
398
+ // The lock changed hands (or was handed back) while the view started: undo it.
399
+ if (!now || now.since !== lock.since || now.userId !== lock.userId) {
400
+ if (r.ok && drive.active(lock.session)) await drive.stop();
401
+ return null;
402
+ }
403
+ if (!r.ok) return null;
404
+ now.fit = r.fit;
405
+ if (r.bounds) now.bounds = r.bounds;
406
+ lockFile.writeLock(now);
407
+ return r.fit;
408
+ }
409
+
410
+ // After a bot restart the lock remembers a view that is no longer live: bring it back. Once at a
411
+ // time, in the background.
412
+ let reviving = null;
413
+ function reviveView(lock) {
414
+ if (!lock || !lock.fit || reviving || drive.active(lock.session) || !drive.supported()) return;
415
+ reviving = startView(lock, lock.fit)
416
+ .catch((e) => console.error(`[browser] bringing the driver's view back failed: ${e.message}`))
417
+ .finally(() => { reviving = null; });
418
+ }
419
+
420
+ async function take({ session, who, viewport } = {}) {
309
421
  if (!validSession(session) || !validWho(who, { needChat: true })) return reply(400, { error: "bad-request" });
310
422
  // A lock that ran out but has not been swept yet is handed back first, the sweeper's way: its
311
423
  // driver's chat is woken and their recording stopped, whoever takes over now.
@@ -324,9 +436,25 @@ async function take({ session, who } = {}) {
324
436
  since: current && current.session === session ? current.since : iso(now),
325
437
  expiresAt: iso(now + LOCK_TTL_MS),
326
438
  };
439
+ // The same person taking again (a reloaded page) keeps the window's size from before their view.
440
+ if (current && current.session === session && sameUser(current, who) && current.bounds) lock.bounds = current.bounds;
327
441
  lockFile.writeLock(lock);
328
442
  openInputSocket(session);
329
- return reply(200, lockBody(lock));
443
+ if (!viewport) return reply(200, lockBody(lock));
444
+ // fit: the driver's view now live, or null (this bot's browser keeps its own size).
445
+ const fitted = await startView(lock, viewport);
446
+ return reply(200, { ...lockBody(lock), fit: fitted });
447
+ }
448
+
449
+ // The driver's screen changed (a turned phone, a resized panel): { session, who, viewport }.
450
+ async function fitView({ session, who, viewport } = {}) {
451
+ if (!validSession(session) || !validWho(who)) return reply(400, { error: "bad-request" });
452
+ if (!drive.checkViewport(viewport)) return reply(400, { error: "bad-viewport" });
453
+ const lock = extendDriverLock(session, who);
454
+ if (!lock) return reply(409, { driving: false, error: "not-driving" });
455
+ if (!drive.supported()) return reply(200, { ...lockBody(lock), fit: null });
456
+ const fitted = await startView(lock, viewport);
457
+ return reply(200, { ...lockBody(lock), fit: fitted });
330
458
  }
331
459
 
332
460
  function renew({ session, who } = {}) {
@@ -339,7 +467,8 @@ function renew({ session, who } = {}) {
339
467
  current.expiresAt = iso(now + LOCK_TTL_MS);
340
468
  lockFile.writeLock(current);
341
469
  openInputSocket(current.session); // after a restart the socket comes back here
342
- return reply(200, lockBody(current));
470
+ reviveView(current); // and the driver's view
471
+ return reply(200, current.fit ? { ...lockBody(current), fit: drive.active(current.session) ? current.fit : null } : lockBody(current));
343
472
  }
344
473
 
345
474
  // Hand the browser back: claim the lock (synchronously, so a release and the sweeper never both
@@ -353,6 +482,13 @@ async function handBack(expected, { reason = null } = {}) {
353
482
  let stopped = null;
354
483
  await runQueued(current.session, 0, async () => {
355
484
  closeInputSocket(current.session);
485
+ // The bot's tab goes back to its own size before the bot carries on.
486
+ try {
487
+ if (drive.active(current.session)) await drive.stop();
488
+ else if (current.bounds) await drive.restoreWindow(info, current.bounds);
489
+ } catch (e) {
490
+ console.error(`[browser] ending the driver's view failed: ${e.message}`);
491
+ }
356
492
  try {
357
493
  stopped = await teach.stopRecording(current.userId, { session: info });
358
494
  } catch (e) {
@@ -360,9 +496,10 @@ async function handBack(expected, { reason = null } = {}) {
360
496
  }
361
497
  });
362
498
  const line = handBackLine(current.name, reason);
499
+ const waited = botWasWaiting(current);
363
500
  if (stopped && !stopped.discarded) wakeChat(current, teach.teachPrompt(stopped, { handBack: line }), "Teach me");
364
- else wakeChat(current, line, "Browser");
365
- console.log(`[browser] ${current.userId} handed the browser back${reason ? ` (${reason})` : ""}`);
501
+ else if (waited) wakeChat(current, line, "Browser");
502
+ console.log(`[browser] ${current.userId} handed the browser back${reason ? ` (${reason})` : ""}${waited || (stopped && !stopped.discarded) ? "" : "; the bot was not waiting for them, so it is not woken"}`);
366
503
  return true;
367
504
  }
368
505
 
@@ -379,15 +516,20 @@ async function control(body = {}) {
379
516
  if (body.action === "take") return take(body);
380
517
  if (body.action === "renew") return renew(body);
381
518
  if (body.action === "release") return release(body);
519
+ if (body.action === "fit") return fitView(body);
382
520
  return reply(400, { error: "bad-action" });
383
521
  }
384
522
 
523
+ // navigate: true tells the front door this bot has the address bar (POST /api/browser/navigate);
524
+ // fit: true that a take can bring the driver's view (core/browser-drive.js) and input_text works.
385
525
  function status() {
386
526
  const lock = lockFile.liveLock(deps.now());
387
527
  return reply(200, {
388
528
  driving: !!lock,
389
529
  driver: lock ? { name: lock.name, userId: lock.userId } : null,
390
530
  recording: teach.activeSummary(),
531
+ navigate: true,
532
+ fit: drive.supported(),
391
533
  });
392
534
  }
393
535
 
@@ -403,31 +545,42 @@ async function input({ session, events, who } = {}) {
403
545
  if (!msg) return reply(400, { error: "bad-event", index: i });
404
546
  messages.push(msg);
405
547
  }
406
- const now = deps.now();
407
- const lock = lockFile.liveLock(now);
408
- if (!lock || lock.session !== session || !sameUser(lock, who)) return reply(409, { error: "not-driving" });
409
- // Every accepted input extends the lock; the file is rewritten only every few seconds.
410
- if (lockFile.lockExpiresAt(lock) - now < LOCK_TTL_MS - LOCK_REWRITE_MS) {
411
- lock.expiresAt = iso(now + LOCK_TTL_MS);
412
- lockFile.writeLock(lock);
413
- }
548
+ // Every accepted input extends the lock.
549
+ const lock = extendDriverLock(session, who);
550
+ if (!lock) return reply(409, { error: "not-driving" });
414
551
  if (!messages.length) return reply(200, { ok: true, accepted: 0 });
552
+ if (lock.fit && !drive.active(session)) reviveView(lock);
415
553
  const entry = openInputSocket(session);
416
- if (!entry) return reply(404, { error: "not-running" });
554
+ if (!entry && !drive.active(session)) return reply(404, { error: "not-running" });
417
555
  const job = runQueued(session, messages.length, async () => {
418
- if (!(await socketReady(entry))) return reply(503, { error: "browser-unavailable", accepted: 0 });
419
556
  const recorder = teach.recorderFor(session, who.userId);
420
- let sent = 0;
421
- for (const msg of messages) {
422
- if (recorder) {
557
+ const record = async (msg, frame, info) => {
558
+ if (!recorder) return;
559
+ // Teach me counts typing from key presses: typed text is shown to it as keys.
560
+ for (const ev of msg.type === "input_text" ? textAsKeys(msg.text) : [msg]) {
423
561
  try {
424
- await recorder.onEvent(msg, { session: { name: entry.name, dir: entry.dir }, frame: entry.frame });
562
+ await recorder.onEvent(ev, { session: info, frame });
425
563
  } catch (e) {
426
564
  console.error(`[browser] recording a step failed: ${e.message}`);
427
565
  }
428
566
  }
567
+ };
568
+ // The driver's view: straight to Chrome, in order, without waiting for each answer.
569
+ if (drive.active(session)) {
570
+ const info = sessionInfo(session) || { name: session, dir: entry ? entry.dir : null };
571
+ for (const msg of messages) {
572
+ await record(msg, drive.lastFrame(), { name: info.name, dir: info.dir });
573
+ if (drive.dispatch([msg]) < 0) return reply(503, { error: "browser-unavailable" });
574
+ }
575
+ return reply(200, { ok: true, accepted: messages.length });
576
+ }
577
+ if (!entry || !(await socketReady(entry))) return reply(503, { error: "browser-unavailable", accepted: 0 });
578
+ let sent = 0;
579
+ for (const msg of messages) {
580
+ await record(msg, entry.frame, { name: entry.name, dir: entry.dir });
429
581
  if (!entry.open || entry.closed) return reply(503, { error: "browser-unavailable", accepted: sent });
430
- entry.ws.send(JSON.stringify(msg));
582
+ // agent-browser's socket takes no text: typed text goes as key presses.
583
+ for (const ev of msg.type === "input_text" ? textAsKeys(msg.text) : [msg]) entry.ws.send(JSON.stringify(ev));
431
584
  sent += 1;
432
585
  }
433
586
  return reply(200, { ok: true, accepted: sent });
@@ -436,6 +589,72 @@ async function input({ session, events, who } = {}) {
436
589
  return job;
437
590
  }
438
591
 
592
+ // ---------------------------------------------------------------------------
593
+ // The address bar: open an address, back, forward, reload
594
+
595
+ // Log words. An address is cut to origin + path: query strings and fragments can carry tokens.
596
+ function navigationWords(action, href) {
597
+ if (action === "open") {
598
+ const where = view.safePageAddress(href) || "an address";
599
+ return { done: `went to ${where}`, failed: `go to ${where}` };
600
+ }
601
+ if (action === "reload") return { done: "reloaded the page", failed: "reload the page" };
602
+ return { done: `went ${action}`, failed: `go ${action}` };
603
+ }
604
+
605
+ // { session, action: "open" | "back" | "forward" | "reload", url (open), who: { userId } }
606
+ async function navigate({ session, action, url, who } = {}) {
607
+ if (!validSession(session) || !validWho(who)) return reply(400, { error: "bad-request" });
608
+ if (!NAVIGATIONS.has(action)) return reply(400, { error: "bad-action" });
609
+ let target = null;
610
+ if (action === "open") {
611
+ target = address.parseAddress(url);
612
+ if (target.error) return reply(target.error === "bad-url" ? 400 : 403, { error: target.error });
613
+ }
614
+ // Like an input, a navigation needs the live lock and extends it.
615
+ if (!extendDriverLock(session, who)) return reply(409, { error: "not-driving" });
616
+ if (!openInputSocket(session)) return reply(404, { error: "not-running" });
617
+ // The name is resolved from now on, while the input queued before this still runs.
618
+ const resolving = target && target.lookupName
619
+ ? address.resolveAddress(target.lookupName, { lookup: deps.lookupHost || undefined })
620
+ : null;
621
+ const href = target ? target.url.href : null;
622
+ const job = runQueued(session, 1, async () => {
623
+ if (resolving) {
624
+ const found = await resolving;
625
+ if (found.error) return reply(found.error === "unknown-host" ? 400 : 403, { error: found.error });
626
+ }
627
+ // agent-browser starts a new browser for a session it does not know: one that ended while
628
+ // this waited is not asked.
629
+ const info = sessionInfo(session);
630
+ if (!info || !info.dir) return reply(404, { error: "not-running" });
631
+ const recorder = teach.recorderFor(session, who.userId);
632
+ if (recorder) {
633
+ const live = sockets.get(session);
634
+ try {
635
+ await recorder.onNavigate({ how: action, to: href }, { session: { name: info.name, dir: info.dir }, frame: live && !live.closed ? live.frame : null });
636
+ } catch (e) {
637
+ console.error(`[browser] recording a step failed: ${e.message}`);
638
+ }
639
+ }
640
+ const words = navigationWords(action, href);
641
+ const run = await runAgentBrowser(info, action === "open" ? ["open", href] : [action], {
642
+ timeoutMs: deps.navigateTimeoutMs,
643
+ execFile: deps.execFile || undefined,
644
+ });
645
+ if (!run.ok) {
646
+ // Never the command's own output or error: both can repeat the whole address.
647
+ const why = run.timedOut ? `agent-browser took longer than ${Math.ceil(deps.navigateTimeoutMs / 1000)} s and was stopped` : "agent-browser failed";
648
+ console.error(`[browser] ${who.userId} could not ${words.failed}: ${why}`);
649
+ return reply(503, { error: "browser-unavailable" });
650
+ }
651
+ console.log(`[browser] ${who.userId} ${words.done}`);
652
+ return reply(200, { ok: true });
653
+ });
654
+ if (!job) return reply(429, { error: "busy" });
655
+ return job;
656
+ }
657
+
439
658
  // ---------------------------------------------------------------------------
440
659
  // C4: Teach me, start / stop / cancel
441
660
 
@@ -501,7 +720,8 @@ function startSweeper({ intervalMs = SWEEP_INTERVAL_MS } = {}) {
501
720
  return sweepTimer;
502
721
  }
503
722
 
504
- // Tests swap the clock, the session list, the socket factory, the scheduler and the run lookup.
723
+ // Tests swap the clock, the session list, the socket factory, the scheduler, the run lookup, the
724
+ // agent-browser call (execFile), the DNS lookup and the address-bar time limit.
505
725
  function _setDeps(overrides = {}) {
506
726
  deps = { ...deps, ...overrides };
507
727
  }
@@ -509,24 +729,23 @@ function _reset() {
509
729
  for (const name of [...sockets.keys()]) closeInputSocket(name);
510
730
  queues.clear();
511
731
  if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
512
- deps = {
513
- now: () => Date.now(),
514
- listSessions: () => view.listSessions(),
515
- connect: (url) => new WebSocket(url),
516
- scheduler: null,
517
- runFor: null,
518
- };
732
+ reviving = null;
733
+ drive._reset();
734
+ deps = defaultDeps();
519
735
  }
520
736
 
521
737
  module.exports = {
522
738
  LOCK_TTL_MS,
523
739
  MAX_EVENTS,
740
+ NAVIGATE_TIMEOUT_MS,
524
741
  SWEEP_INTERVAL_MS,
525
742
  checkEvent,
526
743
  chatOrigin,
527
744
  control,
745
+ fitView,
528
746
  handBackLine,
529
747
  input,
748
+ navigate,
530
749
  release,
531
750
  renew,
532
751
  startSweeper,