@inetafrica/open-claudia 3.5.58 → 3.5.60
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.
- package/CHANGELOG.md +17 -0
- package/channels/kazee/adapter.js +24 -0
- package/core/browser-control.js +201 -17
- package/core/browser-drive.js +619 -0
- package/core/browser-lock.js +33 -2
- package/core/browser-view.js +90 -5
- package/core/cards.js +18 -0
- package/core/enforcer.js +3 -0
- package/core/loopback.js +4 -1
- package/core/relationship.js +41 -18
- package/core/system-prompt.js +20 -8
- package/core/tool-guard.js +3 -0
- package/core/utility-agent.js +5 -2
- package/package.json +2 -2
- package/test-external-rule-scopes.js +24 -0
- package/test-provider-language.js +5 -1
- package/web.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.5.60
|
|
4
|
+
|
|
5
|
+
3.5.59 plus the Browser app work that only the Browser canary bot had (3.5.25, 3.5.26, 3.5.28). 3.5.33 was built on 3.5.24, 3.5.58 on 3.5.33 and 3.5.59 on 3.5.58, so every other bot still said "<bot>'s browser isn't open right now" and had no phone view (Sumeet, 13 Oct 17:4x EAT on sales-bot: "thought we fixed this?").
|
|
6
|
+
|
|
7
|
+
- **Always on (3.5.28).** `POST /api/browser/start`: opening the Browser app while the bot is not browsing opens the bot's own default session on `https://www.google.com/`, with the bot's agent-browser environment and socket folder, so the bot carries on in that tab. `GET /api/browser/control` says `start: true`; Agent Space's front door (223b978) asks only bots that say so.
|
|
8
|
+
- **The bot's tab takes the driver's screen (3.5.25).** With Chrome in its own container, a take or a `fit` fits the bot's tab to the driver's screen (`core/browser-drive.js`): phone layout on a phone, the panel's size on the web; handing back puts it back. Input goes straight to Chrome in order, scaled by the view's density; text fields open the phone's own keyboard; a hand back wakes the bot only when it waits for that person.
|
|
9
|
+
- **Sharp after every page (3.5.26).** After a main-frame load the window is nudged a pixel, so Chrome 150 keeps the view's density.
|
|
10
|
+
- **Tests:** `test-browser-drive.js` (now in `npm test`), `test-browser-control.js`, `test-web-browser-app.js`.
|
|
11
|
+
|
|
12
|
+
## 3.5.59
|
|
13
|
+
|
|
14
|
+
3.5.58 plus three guard fixes from kazee-tickets-bot's Ops monitoring room (13 Oct; Sumeet, 16:39 EAT: "fix the other bug also but approvals need to go to the owners direct chat"). Canary on kazee-tickets-bot first.
|
|
15
|
+
|
|
16
|
+
- **Approval cards go to each owner's direct chat, never a group.** An owner whose stored primary channel was the Ops room had every card (the held message, the draft reply, the guard's reason) posted in front of the whole room; another owner's primary was his user id, which 404s as a chat, so he never got one. Owner-only posts now take the owner's first handle that is a 1:1 chat with the bot: Kazee asks the server (a group, or a chat with more than two members, is refused; a user id opens the bot's DM with them), Telegram group ids are refused, and an owner reachable only in a group gets no card. Presses were already owner-only. Test: `test-owner-cards-dm.js` (new).
|
|
17
|
+
- **The reply writer reads the room's rules.** Room rules and global guardrails reached only the guard, so an unverified person tagging the bot in a room whose rules clear their question got the hello-only lane and "I'll check with the owner", and the guard then flagged the writer for deferring (also 25 Sep). The restricted-mode block now carries both layers through the guard's own loaders. Test: `test-external-rule-scopes.js`.
|
|
18
|
+
- **The guard retries once on Claude Code's sign-in refresh race** instead of sending the owner a card (network-mon, 12 Oct 15:47 EAT). Test: `test-limit-waits.js`.
|
|
19
|
+
|
|
3
20
|
## 3.5.58
|
|
4
21
|
|
|
5
22
|
The call work from the owner's own bot (local builds 3.5.34-3.5.57), released for every bot (Sumeet, 13 Oct 11:53 EAT, canary pod first). Also carries 3.5.33's quiet chats and 3.5.47's guard fix.
|
|
@@ -1552,6 +1552,30 @@ class KazeeAdapter {
|
|
|
1552
1552
|
// chatId. chat-central's createChat returns the existing DM when one
|
|
1553
1553
|
// already exists, so this is idempotent. Requires chats.create on the bot
|
|
1554
1554
|
// token — callers must treat null as "outreach unavailable", not fatal.
|
|
1555
|
+
// The 1:1 chat an owner-only post (an approval card) may go to, for an id
|
|
1556
|
+
// that is either a chat document or a user. A group answers null: a card
|
|
1557
|
+
// there shows every member what the owner is being asked, the held message
|
|
1558
|
+
// and the draft reply (kazee-tickets-bot, 13 Oct: an owner's primary
|
|
1559
|
+
// channel was the Ops room, so every card landed in front of the room). A
|
|
1560
|
+
// 1:1 chat is itself. An id the server doesn't know as a chat is a user,
|
|
1561
|
+
// opened as the bot's DM with them. Any other failure answers null, never a
|
|
1562
|
+
// guess.
|
|
1563
|
+
async directChatFor(id) {
|
|
1564
|
+
const chatId = String(id || "");
|
|
1565
|
+
if (!chatId) return null;
|
|
1566
|
+
try {
|
|
1567
|
+
const res = await this._request("GET", `/chats/${encodeURIComponent(chatId)}`);
|
|
1568
|
+
const chat = res?.chat || res || {};
|
|
1569
|
+
if (chat.error) throw new Error(String(chat.error));
|
|
1570
|
+
const members = Array.isArray(chat.participants) ? chat.participants.length : 0;
|
|
1571
|
+
return chat.isGroupChat || members > 2 ? null : chatId;
|
|
1572
|
+
} catch (e) {
|
|
1573
|
+
if (/→ 404\b/.test(String(e.message || ""))) return this.openDirectChat(chatId);
|
|
1574
|
+
console.error(`Kazee direct chat lookup failed (${chatId}):`, e.message);
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1555
1579
|
async openDirectChat(userId) {
|
|
1556
1580
|
if (!this.botUserId || !userId) return null;
|
|
1557
1581
|
try {
|
package/core/browser-control.js
CHANGED
|
@@ -13,18 +13,30 @@
|
|
|
13
13
|
// after the input before them, with the bot's own agent-browser environment and never a
|
|
14
14
|
// launch flag (core/agent-browser-cli.js: other launch settings would restart the browser).
|
|
15
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.
|
|
20
|
+
// - always on: a person who opens the Browser app while the bot's browser is not open gets it
|
|
21
|
+
// opened on a start page, in the bot's own default session (the one its `agent-browser open`
|
|
22
|
+
// uses), with the bot's own agent-browser environment, so the bot carries on in that tab.
|
|
16
23
|
// - hand back (release, a closed panel, or a lock found expired by the 30 s sweeper): the lock
|
|
17
|
-
// goes, the socket
|
|
18
|
-
//
|
|
24
|
+
// goes, the view and the socket close, any recording stops, and the bot is woken in the
|
|
25
|
+
// person's own chat (C6), but only when it waits for them: it sent them an Open Browser card,
|
|
26
|
+
// or one of its browser commands was refused while they drove. A recording stopped at the
|
|
27
|
+
// same time folds into one wake-up. Otherwise the hand back is only logged.
|
|
19
28
|
//
|
|
20
29
|
// Every route answers { status, body }; web.js writes it. Agent Space reaches these routes with
|
|
21
30
|
// the control token only.
|
|
22
31
|
|
|
32
|
+
const os = require("os");
|
|
33
|
+
const path = require("path");
|
|
23
34
|
const WebSocket = require("ws");
|
|
24
35
|
const lockFile = require("./browser-lock");
|
|
25
36
|
const view = require("./browser-view");
|
|
26
37
|
const teach = require("./browser-teach");
|
|
27
38
|
const address = require("./browser-address");
|
|
39
|
+
const drive = require("./browser-drive");
|
|
28
40
|
const { runAgentBrowser } = require("./agent-browser-cli");
|
|
29
41
|
|
|
30
42
|
const LOCK_TTL_MS = lockFile.LOCK_TTL_MS;
|
|
@@ -41,6 +53,13 @@ const ID_RE = /^[A-Za-z0-9._:-]{1,100}$/;
|
|
|
41
53
|
// An address-bar command still running after this is killed, and the person told it failed.
|
|
42
54
|
const NAVIGATE_TIMEOUT_MS = 20 * 1000;
|
|
43
55
|
const NAVIGATIONS = new Set(["open", "back", "forward", "reload"]);
|
|
56
|
+
// Always on: the page a browser opened for a person starts on, and the session it opens (the one
|
|
57
|
+
// the bot's own `agent-browser` commands use when they name none).
|
|
58
|
+
const START_PAGE = "https://www.google.com/";
|
|
59
|
+
const START_SESSION = "default";
|
|
60
|
+
|
|
61
|
+
// An Open Browser card counts as a request for a person this long.
|
|
62
|
+
const REQUEST_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
44
63
|
|
|
45
64
|
const HAND_BACK_REASONS = {
|
|
46
65
|
closed: " (they closed the Browser)",
|
|
@@ -107,6 +126,7 @@ const MOUSE_EVENTS = new Set(["mousePressed", "mouseReleased", "mouseMoved", "mo
|
|
|
107
126
|
const MOUSE_BUTTONS = new Set(["none", "left", "middle", "right"]);
|
|
108
127
|
const KEY_EVENTS = new Set(["keyDown", "keyUp", "char"]);
|
|
109
128
|
const TOUCH_EVENTS = new Set(["touchStart", "touchMove", "touchEnd", "touchCancel"]);
|
|
129
|
+
const TEXT_MAX = drive.TEXT_MAX;
|
|
110
130
|
|
|
111
131
|
const isNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
112
132
|
const inRange = (v, lo, hi) => isNumber(v) && v >= lo && v <= hi;
|
|
@@ -163,11 +183,34 @@ function checkEvent(ev) {
|
|
|
163
183
|
}
|
|
164
184
|
const out = { type: "input_touch", eventType: ev.eventType, touchPoints };
|
|
165
185
|
if (!optional(out, "modifiers", modifiers)) return null;
|
|
186
|
+
// When the finger moved, on the driver's device (ms): keeps a swipe's speed (core/browser-drive.js).
|
|
187
|
+
if (!optional(out, "t", (v) => inRange(v, 0, 1e13))) return null;
|
|
166
188
|
return out;
|
|
167
189
|
}
|
|
190
|
+
// Text the driver typed (their phone keyboard, a paste), inserted where the page's caret is.
|
|
191
|
+
if (ev.type === "input_text") {
|
|
192
|
+
if (typeof ev.text !== "string" || !ev.text || Array.from(ev.text).length > TEXT_MAX) return null;
|
|
193
|
+
return { type: "input_text", text: ev.text };
|
|
194
|
+
}
|
|
168
195
|
return null;
|
|
169
196
|
}
|
|
170
197
|
|
|
198
|
+
// input_text as key presses, for a bot without the driver's view (agent-browser's socket takes
|
|
199
|
+
// no text) and for Teach me, which counts typing from key presses.
|
|
200
|
+
function textAsKeys(text) {
|
|
201
|
+
const out = [];
|
|
202
|
+
for (const ch of Array.from(text)) {
|
|
203
|
+
if (ch === "\n" || ch === "\r") {
|
|
204
|
+
out.push({ type: "input_keyboard", eventType: "keyDown", key: "Enter", code: "Enter", text: "\r", windowsVirtualKeyCode: 13 });
|
|
205
|
+
out.push({ type: "input_keyboard", eventType: "keyUp", key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 });
|
|
206
|
+
} else {
|
|
207
|
+
out.push({ type: "input_keyboard", eventType: "keyDown", key: ch, text: ch });
|
|
208
|
+
out.push({ type: "input_keyboard", eventType: "keyUp", key: ch });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
|
|
171
214
|
// ---------------------------------------------------------------------------
|
|
172
215
|
// Input sockets: one per agent-browser session, opened on take (or on the first input after a
|
|
173
216
|
// restart), closed on hand back.
|
|
@@ -325,7 +368,23 @@ function wakeChat({ chatId, userId }, prompt, label) {
|
|
|
325
368
|
|
|
326
369
|
function handBackLine(name, reason) {
|
|
327
370
|
const who = teach.cleanName(name) || "Someone";
|
|
328
|
-
return `[Browser] ${who} handed the browser back${HAND_BACK_REASONS[reason] || ""}.
|
|
371
|
+
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.`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Was the bot waiting for this person when they handed back? It asked for them with an Open
|
|
375
|
+
// Browser card in their chat (since the day before they took over), or one of its own browser
|
|
376
|
+
// commands was refused while they drove.
|
|
377
|
+
function botWasWaiting(lock) {
|
|
378
|
+
let asked = [];
|
|
379
|
+
try {
|
|
380
|
+
const since = Date.parse(lock.since);
|
|
381
|
+
asked = require("./cards").takeBrowserRequests(lock.chatId, (Number.isFinite(since) ? since : deps.now()) - REQUEST_MAX_AGE_MS, deps.now());
|
|
382
|
+
} catch (e) {
|
|
383
|
+
console.error(`[browser] reading the bot's Open Browser cards failed: ${e.message}`);
|
|
384
|
+
}
|
|
385
|
+
const refused = lockFile.waitedDuring(lock);
|
|
386
|
+
lockFile.clearWaiting();
|
|
387
|
+
return asked.length > 0 || refused;
|
|
329
388
|
}
|
|
330
389
|
|
|
331
390
|
// ---------------------------------------------------------------------------
|
|
@@ -335,7 +394,39 @@ function lockBody(lock) {
|
|
|
335
394
|
return { driving: true, driver: { name: lock.name } };
|
|
336
395
|
}
|
|
337
396
|
|
|
338
|
-
|
|
397
|
+
// The driver's view on this lock's session: start it (or move it to a new screen) and remember it
|
|
398
|
+
// in the lock, so a restarted bot can bring it back. -> the fit, or null (the browser keeps its size).
|
|
399
|
+
async function startView(lock, viewport) {
|
|
400
|
+
if (!viewport || !drive.supported()) return null;
|
|
401
|
+
const info = sessionInfo(lock.session);
|
|
402
|
+
if (!info) return null;
|
|
403
|
+
const r = drive.active(lock.session)
|
|
404
|
+
? await drive.fit(viewport)
|
|
405
|
+
: await drive.start({ session: info, viewport, savedBounds: lock.bounds || null });
|
|
406
|
+
const now = lockFile.readLock();
|
|
407
|
+
// The lock changed hands (or was handed back) while the view started: undo it.
|
|
408
|
+
if (!now || now.since !== lock.since || now.userId !== lock.userId) {
|
|
409
|
+
if (r.ok && drive.active(lock.session)) await drive.stop();
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
if (!r.ok) return null;
|
|
413
|
+
now.fit = r.fit;
|
|
414
|
+
if (r.bounds) now.bounds = r.bounds;
|
|
415
|
+
lockFile.writeLock(now);
|
|
416
|
+
return r.fit;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// After a bot restart the lock remembers a view that is no longer live: bring it back. Once at a
|
|
420
|
+
// time, in the background.
|
|
421
|
+
let reviving = null;
|
|
422
|
+
function reviveView(lock) {
|
|
423
|
+
if (!lock || !lock.fit || reviving || drive.active(lock.session) || !drive.supported()) return;
|
|
424
|
+
reviving = startView(lock, lock.fit)
|
|
425
|
+
.catch((e) => console.error(`[browser] bringing the driver's view back failed: ${e.message}`))
|
|
426
|
+
.finally(() => { reviving = null; });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function take({ session, who, viewport } = {}) {
|
|
339
430
|
if (!validSession(session) || !validWho(who, { needChat: true })) return reply(400, { error: "bad-request" });
|
|
340
431
|
// A lock that ran out but has not been swept yet is handed back first, the sweeper's way: its
|
|
341
432
|
// driver's chat is woken and their recording stopped, whoever takes over now.
|
|
@@ -354,9 +445,25 @@ async function take({ session, who } = {}) {
|
|
|
354
445
|
since: current && current.session === session ? current.since : iso(now),
|
|
355
446
|
expiresAt: iso(now + LOCK_TTL_MS),
|
|
356
447
|
};
|
|
448
|
+
// The same person taking again (a reloaded page) keeps the window's size from before their view.
|
|
449
|
+
if (current && current.session === session && sameUser(current, who) && current.bounds) lock.bounds = current.bounds;
|
|
357
450
|
lockFile.writeLock(lock);
|
|
358
451
|
openInputSocket(session);
|
|
359
|
-
return reply(200, lockBody(lock));
|
|
452
|
+
if (!viewport) return reply(200, lockBody(lock));
|
|
453
|
+
// fit: the driver's view now live, or null (this bot's browser keeps its own size).
|
|
454
|
+
const fitted = await startView(lock, viewport);
|
|
455
|
+
return reply(200, { ...lockBody(lock), fit: fitted });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// The driver's screen changed (a turned phone, a resized panel): { session, who, viewport }.
|
|
459
|
+
async function fitView({ session, who, viewport } = {}) {
|
|
460
|
+
if (!validSession(session) || !validWho(who)) return reply(400, { error: "bad-request" });
|
|
461
|
+
if (!drive.checkViewport(viewport)) return reply(400, { error: "bad-viewport" });
|
|
462
|
+
const lock = extendDriverLock(session, who);
|
|
463
|
+
if (!lock) return reply(409, { driving: false, error: "not-driving" });
|
|
464
|
+
if (!drive.supported()) return reply(200, { ...lockBody(lock), fit: null });
|
|
465
|
+
const fitted = await startView(lock, viewport);
|
|
466
|
+
return reply(200, { ...lockBody(lock), fit: fitted });
|
|
360
467
|
}
|
|
361
468
|
|
|
362
469
|
function renew({ session, who } = {}) {
|
|
@@ -369,7 +476,8 @@ function renew({ session, who } = {}) {
|
|
|
369
476
|
current.expiresAt = iso(now + LOCK_TTL_MS);
|
|
370
477
|
lockFile.writeLock(current);
|
|
371
478
|
openInputSocket(current.session); // after a restart the socket comes back here
|
|
372
|
-
|
|
479
|
+
reviveView(current); // and the driver's view
|
|
480
|
+
return reply(200, current.fit ? { ...lockBody(current), fit: drive.active(current.session) ? current.fit : null } : lockBody(current));
|
|
373
481
|
}
|
|
374
482
|
|
|
375
483
|
// Hand the browser back: claim the lock (synchronously, so a release and the sweeper never both
|
|
@@ -383,6 +491,13 @@ async function handBack(expected, { reason = null } = {}) {
|
|
|
383
491
|
let stopped = null;
|
|
384
492
|
await runQueued(current.session, 0, async () => {
|
|
385
493
|
closeInputSocket(current.session);
|
|
494
|
+
// The bot's tab goes back to its own size before the bot carries on.
|
|
495
|
+
try {
|
|
496
|
+
if (drive.active(current.session)) await drive.stop();
|
|
497
|
+
else if (current.bounds) await drive.restoreWindow(info, current.bounds);
|
|
498
|
+
} catch (e) {
|
|
499
|
+
console.error(`[browser] ending the driver's view failed: ${e.message}`);
|
|
500
|
+
}
|
|
386
501
|
try {
|
|
387
502
|
stopped = await teach.stopRecording(current.userId, { session: info });
|
|
388
503
|
} catch (e) {
|
|
@@ -390,9 +505,10 @@ async function handBack(expected, { reason = null } = {}) {
|
|
|
390
505
|
}
|
|
391
506
|
});
|
|
392
507
|
const line = handBackLine(current.name, reason);
|
|
508
|
+
const waited = botWasWaiting(current);
|
|
393
509
|
if (stopped && !stopped.discarded) wakeChat(current, teach.teachPrompt(stopped, { handBack: line }), "Teach me");
|
|
394
|
-
else wakeChat(current, line, "Browser");
|
|
395
|
-
console.log(`[browser] ${current.userId} handed the browser back${reason ? ` (${reason})` : ""}`);
|
|
510
|
+
else if (waited) wakeChat(current, line, "Browser");
|
|
511
|
+
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"}`);
|
|
396
512
|
return true;
|
|
397
513
|
}
|
|
398
514
|
|
|
@@ -409,10 +525,13 @@ async function control(body = {}) {
|
|
|
409
525
|
if (body.action === "take") return take(body);
|
|
410
526
|
if (body.action === "renew") return renew(body);
|
|
411
527
|
if (body.action === "release") return release(body);
|
|
528
|
+
if (body.action === "fit") return fitView(body);
|
|
412
529
|
return reply(400, { error: "bad-action" });
|
|
413
530
|
}
|
|
414
531
|
|
|
415
|
-
// navigate: true tells the front door this bot has the address bar (POST /api/browser/navigate)
|
|
532
|
+
// navigate: true tells the front door this bot has the address bar (POST /api/browser/navigate);
|
|
533
|
+
// fit: true that a take can bring the driver's view (core/browser-drive.js) and input_text works;
|
|
534
|
+
// start: true that it opens its browser on a start page when there is none (POST /api/browser/start).
|
|
416
535
|
function status() {
|
|
417
536
|
const lock = lockFile.liveLock(deps.now());
|
|
418
537
|
return reply(200, {
|
|
@@ -420,9 +539,50 @@ function status() {
|
|
|
420
539
|
driver: lock ? { name: lock.name, userId: lock.userId } : null,
|
|
421
540
|
recording: teach.activeSummary(),
|
|
422
541
|
navigate: true,
|
|
542
|
+
fit: drive.supported(),
|
|
543
|
+
start: true,
|
|
423
544
|
});
|
|
424
545
|
}
|
|
425
546
|
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
// Always on: POST /api/browser/start
|
|
549
|
+
|
|
550
|
+
// The socket folder the bot's own agent-browser commands use (its agent runs without
|
|
551
|
+
// XDG_RUNTIME_DIR): AGENT_BROWSER_SOCKET_DIR, else ~/.agent-browser.
|
|
552
|
+
function agentSocketDir(env = process.env, home = os.homedir()) {
|
|
553
|
+
if (env.AGENT_BROWSER_SOCKET_DIR) return env.AGENT_BROWSER_SOCKET_DIR;
|
|
554
|
+
return home ? path.join(home, ".agent-browser") : null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
let starting = null;
|
|
558
|
+
|
|
559
|
+
// { who: { userId } }. A browser already open is answered as it is (the newest session); otherwise
|
|
560
|
+
// the bot's default session opens on the start page, as the bot's own `agent-browser open` would,
|
|
561
|
+
// and two people opening the app at once start one browser. { session, started }.
|
|
562
|
+
async function start({ who } = {}) {
|
|
563
|
+
if (!validWho(who)) return reply(400, { error: "bad-request" });
|
|
564
|
+
const open = deps.listSessions();
|
|
565
|
+
if (open.length) return reply(200, { session: open[0].name, started: false });
|
|
566
|
+
if (!starting) {
|
|
567
|
+
starting = (async () => {
|
|
568
|
+
const dir = agentSocketDir();
|
|
569
|
+
const run = await runAgentBrowser({ name: START_SESSION, dir }, ["open", START_PAGE], {
|
|
570
|
+
timeoutMs: deps.navigateTimeoutMs,
|
|
571
|
+
execFile: deps.execFile || undefined,
|
|
572
|
+
});
|
|
573
|
+
const listed = run.ok ? deps.listSessions().find((s) => s.name === START_SESSION) : null;
|
|
574
|
+
if (!listed) {
|
|
575
|
+
const why = run.timedOut ? `agent-browser took longer than ${Math.ceil(deps.navigateTimeoutMs / 1000)} s and was stopped` : run.ok ? "the browser did not come up" : "agent-browser failed";
|
|
576
|
+
console.error(`[browser] could not open the browser for ${who.userId}: ${why}`);
|
|
577
|
+
return reply(503, { error: "browser-unavailable" });
|
|
578
|
+
}
|
|
579
|
+
console.log(`[browser] opened the browser on the start page for ${who.userId}`);
|
|
580
|
+
return reply(200, { session: listed.name, started: true });
|
|
581
|
+
})().finally(() => { starting = null; });
|
|
582
|
+
}
|
|
583
|
+
return starting;
|
|
584
|
+
}
|
|
585
|
+
|
|
426
586
|
// ---------------------------------------------------------------------------
|
|
427
587
|
// C3: input
|
|
428
588
|
|
|
@@ -436,24 +596,41 @@ async function input({ session, events, who } = {}) {
|
|
|
436
596
|
messages.push(msg);
|
|
437
597
|
}
|
|
438
598
|
// Every accepted input extends the lock.
|
|
439
|
-
|
|
599
|
+
const lock = extendDriverLock(session, who);
|
|
600
|
+
if (!lock) return reply(409, { error: "not-driving" });
|
|
440
601
|
if (!messages.length) return reply(200, { ok: true, accepted: 0 });
|
|
602
|
+
if (lock.fit && !drive.active(session)) reviveView(lock);
|
|
441
603
|
const entry = openInputSocket(session);
|
|
442
|
-
if (!entry) return reply(404, { error: "not-running" });
|
|
604
|
+
if (!entry && !drive.active(session)) return reply(404, { error: "not-running" });
|
|
443
605
|
const job = runQueued(session, messages.length, async () => {
|
|
444
|
-
if (!(await socketReady(entry))) return reply(503, { error: "browser-unavailable", accepted: 0 });
|
|
445
606
|
const recorder = teach.recorderFor(session, who.userId);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
607
|
+
const record = async (msg, frame, info) => {
|
|
608
|
+
if (!recorder) return;
|
|
609
|
+
// Teach me counts typing from key presses: typed text is shown to it as keys.
|
|
610
|
+
for (const ev of msg.type === "input_text" ? textAsKeys(msg.text) : [msg]) {
|
|
449
611
|
try {
|
|
450
|
-
await recorder.onEvent(
|
|
612
|
+
await recorder.onEvent(ev, { session: info, frame });
|
|
451
613
|
} catch (e) {
|
|
452
614
|
console.error(`[browser] recording a step failed: ${e.message}`);
|
|
453
615
|
}
|
|
454
616
|
}
|
|
617
|
+
};
|
|
618
|
+
// The driver's view: straight to Chrome, in order, without waiting for each answer.
|
|
619
|
+
if (drive.active(session)) {
|
|
620
|
+
const info = sessionInfo(session) || { name: session, dir: entry ? entry.dir : null };
|
|
621
|
+
for (const msg of messages) {
|
|
622
|
+
await record(msg, drive.lastFrame(), { name: info.name, dir: info.dir });
|
|
623
|
+
if (drive.dispatch([msg]) < 0) return reply(503, { error: "browser-unavailable" });
|
|
624
|
+
}
|
|
625
|
+
return reply(200, { ok: true, accepted: messages.length });
|
|
626
|
+
}
|
|
627
|
+
if (!entry || !(await socketReady(entry))) return reply(503, { error: "browser-unavailable", accepted: 0 });
|
|
628
|
+
let sent = 0;
|
|
629
|
+
for (const msg of messages) {
|
|
630
|
+
await record(msg, entry.frame, { name: entry.name, dir: entry.dir });
|
|
455
631
|
if (!entry.open || entry.closed) return reply(503, { error: "browser-unavailable", accepted: sent });
|
|
456
|
-
|
|
632
|
+
// agent-browser's socket takes no text: typed text goes as key presses.
|
|
633
|
+
for (const ev of msg.type === "input_text" ? textAsKeys(msg.text) : [msg]) entry.ws.send(JSON.stringify(ev));
|
|
457
634
|
sent += 1;
|
|
458
635
|
}
|
|
459
636
|
return reply(200, { ok: true, accepted: sent });
|
|
@@ -602,6 +779,8 @@ function _reset() {
|
|
|
602
779
|
for (const name of [...sockets.keys()]) closeInputSocket(name);
|
|
603
780
|
queues.clear();
|
|
604
781
|
if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
|
|
782
|
+
reviving = null;
|
|
783
|
+
drive._reset();
|
|
605
784
|
deps = defaultDeps();
|
|
606
785
|
}
|
|
607
786
|
|
|
@@ -609,15 +788,20 @@ module.exports = {
|
|
|
609
788
|
LOCK_TTL_MS,
|
|
610
789
|
MAX_EVENTS,
|
|
611
790
|
NAVIGATE_TIMEOUT_MS,
|
|
791
|
+
START_PAGE,
|
|
792
|
+
START_SESSION,
|
|
612
793
|
SWEEP_INTERVAL_MS,
|
|
794
|
+
agentSocketDir,
|
|
613
795
|
checkEvent,
|
|
614
796
|
chatOrigin,
|
|
615
797
|
control,
|
|
798
|
+
fitView,
|
|
616
799
|
handBackLine,
|
|
617
800
|
input,
|
|
618
801
|
navigate,
|
|
619
802
|
release,
|
|
620
803
|
renew,
|
|
804
|
+
start,
|
|
621
805
|
startSweeper,
|
|
622
806
|
status,
|
|
623
807
|
sweepLock,
|