@inetafrica/open-claudia 3.5.25 → 3.5.27

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.
@@ -13,15 +13,9 @@
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
16
  // - hand back (release, a closed panel, or a lock found expired by the 30 s sweeper): the lock
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.
17
+ // goes, the socket closes, any recording stops, and the bot is woken in the person's own chat
18
+ // to carry on (C6). A recording stopped at the same time folds into one wake-up.
25
19
  //
26
20
  // Every route answers { status, body }; web.js writes it. Agent Space reaches these routes with
27
21
  // the control token only.
@@ -31,7 +25,6 @@ const lockFile = require("./browser-lock");
31
25
  const view = require("./browser-view");
32
26
  const teach = require("./browser-teach");
33
27
  const address = require("./browser-address");
34
- const drive = require("./browser-drive");
35
28
  const { runAgentBrowser } = require("./agent-browser-cli");
36
29
 
37
30
  const LOCK_TTL_MS = lockFile.LOCK_TTL_MS;
@@ -49,9 +42,6 @@ const ID_RE = /^[A-Za-z0-9._:-]{1,100}$/;
49
42
  const NAVIGATE_TIMEOUT_MS = 20 * 1000;
50
43
  const NAVIGATIONS = new Set(["open", "back", "forward", "reload"]);
51
44
 
52
- // An Open Browser card counts as a request for a person this long.
53
- const REQUEST_MAX_AGE_MS = 24 * 60 * 60 * 1000;
54
-
55
45
  const HAND_BACK_REASONS = {
56
46
  closed: " (they closed the Browser)",
57
47
  expired: " (their Browser went quiet for 2 minutes, so control came back on its own)",
@@ -117,7 +107,6 @@ const MOUSE_EVENTS = new Set(["mousePressed", "mouseReleased", "mouseMoved", "mo
117
107
  const MOUSE_BUTTONS = new Set(["none", "left", "middle", "right"]);
118
108
  const KEY_EVENTS = new Set(["keyDown", "keyUp", "char"]);
119
109
  const TOUCH_EVENTS = new Set(["touchStart", "touchMove", "touchEnd", "touchCancel"]);
120
- const TEXT_MAX = drive.TEXT_MAX;
121
110
 
122
111
  const isNumber = (v) => typeof v === "number" && Number.isFinite(v);
123
112
  const inRange = (v, lo, hi) => isNumber(v) && v >= lo && v <= hi;
@@ -174,34 +163,11 @@ function checkEvent(ev) {
174
163
  }
175
164
  const out = { type: "input_touch", eventType: ev.eventType, touchPoints };
176
165
  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;
179
166
  return out;
180
167
  }
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
- }
186
168
  return null;
187
169
  }
188
170
 
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
-
205
171
  // ---------------------------------------------------------------------------
206
172
  // Input sockets: one per agent-browser session, opened on take (or on the first input after a
207
173
  // restart), closed on hand back.
@@ -359,23 +325,7 @@ function wakeChat({ chatId, userId }, prompt, label) {
359
325
 
360
326
  function handBackLine(name, reason) {
361
327
  const who = teach.cleanName(name) || "Someone";
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;
328
+ 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.`;
379
329
  }
380
330
 
381
331
  // ---------------------------------------------------------------------------
@@ -385,39 +335,7 @@ function lockBody(lock) {
385
335
  return { driving: true, driver: { name: lock.name } };
386
336
  }
387
337
 
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 } = {}) {
338
+ async function take({ session, who } = {}) {
421
339
  if (!validSession(session) || !validWho(who, { needChat: true })) return reply(400, { error: "bad-request" });
422
340
  // A lock that ran out but has not been swept yet is handed back first, the sweeper's way: its
423
341
  // driver's chat is woken and their recording stopped, whoever takes over now.
@@ -436,25 +354,9 @@ async function take({ session, who, viewport } = {}) {
436
354
  since: current && current.session === session ? current.since : iso(now),
437
355
  expiresAt: iso(now + LOCK_TTL_MS),
438
356
  };
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;
441
357
  lockFile.writeLock(lock);
442
358
  openInputSocket(session);
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 });
359
+ return reply(200, lockBody(lock));
458
360
  }
459
361
 
460
362
  function renew({ session, who } = {}) {
@@ -467,8 +369,7 @@ function renew({ session, who } = {}) {
467
369
  current.expiresAt = iso(now + LOCK_TTL_MS);
468
370
  lockFile.writeLock(current);
469
371
  openInputSocket(current.session); // after a restart the socket comes back here
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));
372
+ return reply(200, lockBody(current));
472
373
  }
473
374
 
474
375
  // Hand the browser back: claim the lock (synchronously, so a release and the sweeper never both
@@ -482,13 +383,6 @@ async function handBack(expected, { reason = null } = {}) {
482
383
  let stopped = null;
483
384
  await runQueued(current.session, 0, async () => {
484
385
  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
- }
492
386
  try {
493
387
  stopped = await teach.stopRecording(current.userId, { session: info });
494
388
  } catch (e) {
@@ -496,10 +390,9 @@ async function handBack(expected, { reason = null } = {}) {
496
390
  }
497
391
  });
498
392
  const line = handBackLine(current.name, reason);
499
- const waited = botWasWaiting(current);
500
393
  if (stopped && !stopped.discarded) wakeChat(current, teach.teachPrompt(stopped, { handBack: line }), "Teach me");
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"}`);
394
+ else wakeChat(current, line, "Browser");
395
+ console.log(`[browser] ${current.userId} handed the browser back${reason ? ` (${reason})` : ""}`);
503
396
  return true;
504
397
  }
505
398
 
@@ -516,12 +409,10 @@ async function control(body = {}) {
516
409
  if (body.action === "take") return take(body);
517
410
  if (body.action === "renew") return renew(body);
518
411
  if (body.action === "release") return release(body);
519
- if (body.action === "fit") return fitView(body);
520
412
  return reply(400, { error: "bad-action" });
521
413
  }
522
414
 
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.
415
+ // navigate: true tells the front door this bot has the address bar (POST /api/browser/navigate).
525
416
  function status() {
526
417
  const lock = lockFile.liveLock(deps.now());
527
418
  return reply(200, {
@@ -529,7 +420,6 @@ function status() {
529
420
  driver: lock ? { name: lock.name, userId: lock.userId } : null,
530
421
  recording: teach.activeSummary(),
531
422
  navigate: true,
532
- fit: drive.supported(),
533
423
  });
534
424
  }
535
425
 
@@ -546,41 +436,24 @@ async function input({ session, events, who } = {}) {
546
436
  messages.push(msg);
547
437
  }
548
438
  // Every accepted input extends the lock.
549
- const lock = extendDriverLock(session, who);
550
- if (!lock) return reply(409, { error: "not-driving" });
439
+ if (!extendDriverLock(session, who)) return reply(409, { error: "not-driving" });
551
440
  if (!messages.length) return reply(200, { ok: true, accepted: 0 });
552
- if (lock.fit && !drive.active(session)) reviveView(lock);
553
441
  const entry = openInputSocket(session);
554
- if (!entry && !drive.active(session)) return reply(404, { error: "not-running" });
442
+ if (!entry) return reply(404, { error: "not-running" });
555
443
  const job = runQueued(session, messages.length, async () => {
444
+ if (!(await socketReady(entry))) return reply(503, { error: "browser-unavailable", accepted: 0 });
556
445
  const recorder = teach.recorderFor(session, who.userId);
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]) {
446
+ let sent = 0;
447
+ for (const msg of messages) {
448
+ if (recorder) {
561
449
  try {
562
- await recorder.onEvent(ev, { session: info, frame });
450
+ await recorder.onEvent(msg, { session: { name: entry.name, dir: entry.dir }, frame: entry.frame });
563
451
  } catch (e) {
564
452
  console.error(`[browser] recording a step failed: ${e.message}`);
565
453
  }
566
454
  }
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 });
581
455
  if (!entry.open || entry.closed) return reply(503, { error: "browser-unavailable", accepted: sent });
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));
456
+ entry.ws.send(JSON.stringify(msg));
584
457
  sent += 1;
585
458
  }
586
459
  return reply(200, { ok: true, accepted: sent });
@@ -729,8 +602,6 @@ function _reset() {
729
602
  for (const name of [...sockets.keys()]) closeInputSocket(name);
730
603
  queues.clear();
731
604
  if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
732
- reviving = null;
733
- drive._reset();
734
605
  deps = defaultDeps();
735
606
  }
736
607
 
@@ -742,7 +613,6 @@ module.exports = {
742
613
  checkEvent,
743
614
  chatOrigin,
744
615
  control,
745
- fitView,
746
616
  handBackLine,
747
617
  input,
748
618
  navigate,
@@ -4,9 +4,7 @@
4
4
  // hook (core/tool-guard.js, C7), which runs as a separate process for every tool call and
5
5
  // re-reads it each time, like the tooling-mode switch (core/tooling-mode.js).
6
6
  //
7
- // { session, userId, name, chatId, since, expiresAt, fit?, bounds? } (ISO times; atomic, 0600)
8
- // fit: the driver's view (core/browser-drive.js), bounds: the window's size before it, so a
9
- // restarted bot can bring the view back, and the hand back can put the window back.
7
+ // { session, userId, name, chatId, since, expiresAt } (ISO times; written atomically, 0600)
10
8
  //
11
9
  // An expired lock is no lock: it never blocks the bot, and the sweeper in
12
10
  // core/browser-control.js hands the browser back.
@@ -50,33 +48,4 @@ function removeLock() {
50
48
  try { fs.rmSync(driverFile(), { force: true }); } catch (_) { /* already gone */ }
51
49
  }
52
50
 
53
- // The bot was told to wait (core/tool-guard.js refused one of its agent-browser commands while
54
- // this lock was live), so the hand back must wake it. Keyed by the lock's `since`: a marker from
55
- // an earlier drive never counts. A separate file, because the tool guard runs in its own process
56
- // and the lock file is rewritten every second while someone drives.
57
- function waitingFile() {
58
- return process.env.OC_BROWSER_WAITING_FILE || path.join(require("../config-dir"), "browser-waiting.json");
59
- }
60
-
61
- function markWaiting(lock) {
62
- if (!lock || !lock.since) return;
63
- try {
64
- atomicWriteFileSync(waitingFile(), `${JSON.stringify({ since: lock.since, at: new Date().toISOString() })}\n`, { mode: 0o600 });
65
- } catch (_) { /* best effort: the bot then just is not woken */ }
66
- }
67
-
68
- function waitedDuring(lock) {
69
- if (!lock || !lock.since) return false;
70
- try {
71
- const mark = JSON.parse(fs.readFileSync(waitingFile(), "utf8"));
72
- return !!mark && mark.since === lock.since;
73
- } catch (_) {
74
- return false;
75
- }
76
- }
77
-
78
- function clearWaiting() {
79
- try { fs.rmSync(waitingFile(), { force: true }); } catch (_) { /* already gone */ }
80
- }
81
-
82
- module.exports = { LOCK_TTL_MS, driverFile, readLock, lockExpiresAt, liveLock, writeLock, removeLock, markWaiting, waitedDuring, clearWaiting };
51
+ module.exports = { LOCK_TTL_MS, driverFile, readLock, lockExpiresAt, liveLock, writeLock, removeLock };
@@ -22,22 +22,11 @@
22
22
  const fs = require("fs");
23
23
  const os = require("os");
24
24
  const path = require("path");
25
- const { EventEmitter } = require("events");
26
25
  const WebSocket = require("ws");
27
26
 
28
27
  const SESSION_NAME = /^[A-Za-z0-9._-]{1,64}$/;
29
28
  const DEFAULT_FPS = 5;
30
- const MAX_FPS = 30;
31
-
32
- // The phone and web view (core/browser-drive.js) publishes here while a person drives: "frame",
33
- // "fields" (the page's text fields) and "start" / "stop", each with the session's name. A viewer
34
- // of that session gets these instead of agent-browser's own frames, which follow neither the
35
- // driver's size nor the page's real size. `active` and `last` let a viewer that opens mid-drive
36
- // start with the picture at once.
37
- const driveFeed = new EventEmitter();
38
- driveFeed.setMaxListeners(200);
39
- driveFeed.active = null;
40
- driveFeed.last = null;
29
+ const MAX_FPS = 10;
41
30
  const KEEPALIVE_MS = 25000;
42
31
  const ACTIVATE_TIMEOUT_MS = 1500;
43
32
  const TARGET_ID = /^[A-Za-z0-9]{8,64}$/; // static pages send no frames; comments keep proxies from closing the stream
@@ -224,77 +213,18 @@ function openStream(sessionName, { fps, sessions, connect = (url) => new WebSock
224
213
 
225
214
  // Server-sent events for one session: "frame" and "url" events, a final "end" event with the
226
215
  // reason, and ": ping" comments while the page is still. Every payload also carries its type,
227
- // for readers that only look at data lines. While a slow viewer's connection is backed up,
228
- // frames are not queued: only the newest waits, and goes out once the connection drains, so the
229
- // last picture of a scroll is never lost. While a person drives in the phone or web view, the
230
- // frames are the driver's (core/browser-drive.js), at most `fps` a second, with "fields" events
231
- // (the page's text fields) and "fit" events (the driver's view started, changed or ended).
232
- function serveStream(res, sessionName, { fps, sessions, connect, activate, keepaliveMs = KEEPALIVE_MS, feed = driveFeed } = {}) {
216
+ // for readers that only look at data lines. Frames are skipped, never queued, while a slow
217
+ // viewer's connection is backed up.
218
+ function serveStream(res, sessionName, { fps, sessions, connect, activate, keepaliveMs = KEEPALIVE_MS } = {}) {
233
219
  res.writeHead(200, {
234
220
  "Content-Type": "text/event-stream",
235
221
  "Cache-Control": "no-cache, no-store",
236
222
  "Connection": "keep-alive",
237
223
  "X-Accel-Buffering": "no",
238
224
  });
239
- let closed = false;
240
225
  const write = (event, obj) => {
241
- if (closed) return;
242
226
  try { res.write(`event: ${event}\ndata: ${JSON.stringify(obj)}\n\n`); } catch (_) { /* viewer gone */ }
243
227
  };
244
- const interval = 1000 / clampFps(fps);
245
- let lastSent = 0;
246
- let pending = null;
247
- let slot = null;
248
- const flush = () => {
249
- if (!pending || closed || res.writableNeedDrain) return;
250
- const now = Date.now();
251
- const due = lastSent + interval;
252
- if (now < due) {
253
- if (!slot) {
254
- slot = setTimeout(() => { slot = null; flush(); }, due - now);
255
- if (slot.unref) slot.unref();
256
- }
257
- return;
258
- }
259
- const msg = pending;
260
- pending = null;
261
- lastSent = now;
262
- write("frame", msg);
263
- };
264
- const offerFrame = (msg) => {
265
- pending = msg;
266
- flush();
267
- };
268
- if (typeof res.on === "function") res.on("drain", flush);
269
- let driving = feed.active === sessionName;
270
- const onDriveFrame = (ev) => {
271
- if (ev.session !== sessionName || !driving) return;
272
- offerFrame({ type: "frame", seq: ev.frame.seq, data: ev.frame.data, metadata: ev.frame.metadata });
273
- };
274
- const onDriveFields = (ev) => {
275
- if (ev.session !== sessionName || !driving) return;
276
- write("fields", { type: "fields", fields: ev.fields, focused: ev.focused, editable: ev.editable });
277
- };
278
- const onDriveStart = (ev) => {
279
- if (ev.session !== sessionName) return;
280
- driving = true;
281
- write("fit", { type: "fit", fit: ev.fit || null });
282
- };
283
- const onDriveStop = (ev) => {
284
- if (ev.session !== sessionName) return;
285
- driving = false;
286
- write("fit", { type: "fit", fit: null });
287
- };
288
- feed.on("frame", onDriveFrame);
289
- feed.on("fields", onDriveFields);
290
- feed.on("start", onDriveStart);
291
- feed.on("stop", onDriveStop);
292
- const unsubscribe = () => {
293
- feed.removeListener("frame", onDriveFrame);
294
- feed.removeListener("fields", onDriveFields);
295
- feed.removeListener("start", onDriveStart);
296
- feed.removeListener("stop", onDriveStop);
297
- };
298
228
  const heartbeat = setInterval(() => { try { res.write(": ping\n\n"); } catch (_) { /* viewer gone */ } }, keepaliveMs);
299
229
  const stream = openStream(sessionName, {
300
230
  fps,
@@ -302,30 +232,17 @@ function serveStream(res, sessionName, { fps, sessions, connect, activate, keepa
302
232
  connect,
303
233
  ...(activate ? { activate } : {}),
304
234
  onMessage: (msg) => {
305
- if (msg.type === "frame") {
306
- if (!driving) offerFrame(msg);
307
- return;
308
- }
235
+ if (msg.type === "frame" && res.writableNeedDrain) return;
309
236
  write(msg.type, msg);
310
237
  },
311
238
  onEnd: (reason) => {
312
239
  clearInterval(heartbeat);
313
- unsubscribe();
314
240
  write("end", { type: "end", reason });
315
- closed = true;
316
241
  try { res.end(); } catch (_) { /* viewer gone */ }
317
242
  },
318
243
  });
319
- // A viewer that opens while someone drives starts with the driver's picture.
320
- if (driving && feed.last && feed.last.session === sessionName) {
321
- const f = feed.last.frame;
322
- offerFrame({ type: "frame", seq: f.seq, data: f.data, metadata: f.metadata });
323
- }
324
244
  res.on("close", () => {
325
- closed = true;
326
245
  clearInterval(heartbeat);
327
- if (slot) clearTimeout(slot);
328
- unsubscribe();
329
246
  stream.close();
330
247
  });
331
248
  }
@@ -338,12 +255,10 @@ module.exports = {
338
255
  cdpEndpoint,
339
256
  clampFps,
340
257
  describeSessions,
341
- driveFeed,
342
258
  listSessions,
343
259
  openStream,
344
260
  processAlive,
345
261
  safePageAddress,
346
262
  serveStream,
347
- sessionTargetId,
348
263
  socketDirs,
349
264
  };
package/core/cards.js CHANGED
@@ -83,23 +83,6 @@ function recordSentCard(rec) {
83
83
  return cards[String(rec.messageId)];
84
84
  }
85
85
 
86
- // Cards with an Open Browser button the bot sent in this chat since `sinceMs` and that no hand
87
- // back has answered yet: the bot asked for a person in its browser and waits. Each one found is
88
- // marked answered, so one hand back wakes the bot once.
89
- function takeBrowserRequests(channelId, sinceMs, now = Date.now()) {
90
- if (!channelId) return [];
91
- const cards = loadCards();
92
- const found = Object.values(cards).filter((c) =>
93
- c && c.openApp === "browser" && !c.handedBackAt &&
94
- c.origin && String(c.origin.channelId) === String(channelId) &&
95
- Date.parse(c.sentAt || 0) >= sinceMs);
96
- if (!found.length) return [];
97
- const at = new Date(now).toISOString();
98
- for (const c of found) cards[c.messageId] = { ...c, handedBackAt: at };
99
- saveCards(cards);
100
- return found.map((c) => c.messageId);
101
- }
102
-
103
86
  function getCard(messageId) {
104
87
  return loadCards()[String(messageId)] || null;
105
88
  }
@@ -664,7 +647,6 @@ module.exports = {
664
647
  startDecisionPump,
665
648
  DECISIONS_FILE,
666
649
  recordSentCard,
667
- takeBrowserRequests,
668
650
  getCard,
669
651
  updateCardRecord,
670
652
  noteCardUpdate,
package/core/loopback.js CHANGED
@@ -215,10 +215,7 @@ async function handleCardKind(res, kind, payload, { channelId, adapterId, adapte
215
215
  }
216
216
  if (!messageId) return reply(res, 502, { error: "Kazee Chat returned no message id for the card" });
217
217
  const wake = ["each", "first", "none"].includes(payload.wake) ? payload.wake : cards.defaultWake(card);
218
- // openApp: a card with an Open <app> button (the bot asking for a person in its Browser):
219
- // that person's hand back wakes the bot (core/browser-control.js).
220
- const openApp = card.open && typeof card.open === "object" && typeof card.open.app === "string" ? card.open.app.slice(0, 40) : null;
221
- cards.recordSentCard({ messageId, title: card.title, kind: card.kind, wake, origin: cardOrigin(caller, payload, own, threadId), ...(openApp ? { openApp } : {}) });
218
+ cards.recordSentCard({ messageId, title: card.title, kind: card.kind, wake, origin: cardOrigin(caller, payload, own, threadId) });
222
219
  return reply(res, 200, { ok: true, messageId, wake });
223
220
  }
224
221
 
@@ -1246,12 +1246,20 @@ const LIVE_CALL_GUIDANCE = [
1246
1246
  "You are on a LIVE voice call with the user right now. Their speech arrives transcribed; your final reply is synthesized straight onto the open line. Write it to be HEARD:",
1247
1247
  "- Phone-call style: one to three short conversational sentences, then stop. No markdown, bullets, headings, emoji, backticks, or asterisks — they are noise when spoken.",
1248
1248
  "- Do NOT read out code, file paths, command syntax, IDs, or URLs. Describe them in plain words, or offer to send the details to the chat as text.",
1249
- "- Tool time is dead air the caller sits through. Prefer answering from what you already know; keep any lookup to a couple of seconds, and offer to do longer digging after the call.",
1250
- "- When a useful, complete answer sentence is ready before your full response, use open-claudia call-say to speak it now. This command delivers checked speech into this call; ordinary provider previews are not spoken. Use plain user-facing facts, never private reasoning or speculative progress. Report completed actions only after tool evidence.",
1249
+ "- Speak first, then work. Unless your whole reply is one short sentence you can say right now, your FIRST action is a call-say of a few words (about 3 to 12) that shows you heard them and says what you will do, for example \"Sure, let me check the call logs.\", \"Good question, give me a second.\" or \"Yep, on it.\" Do it before any lookup and before working out the answer: think after it, not before.",
1250
+ "- If the work runs on, say a brief progress line with call-say about every ten seconds (\"Still looking, nearly there.\"). Tool time is dead air the caller sits through, so keep lookups short and offer longer digging after the call.",
1251
+ "- When a useful, complete answer sentence is ready before your full response, speak it with call-say too. This command delivers checked speech into this call; ordinary provider previews are not spoken. Acknowledgements and what-you-will-do-next lines are fine; never state a result or claim a completed action before tool evidence.",
1251
1252
  "- Send one short chunk at a time (maximum 450 characters); wait for call-say to finish, never background it. Use --stdin with a quoted heredoc for shell-sensitive text. A refused or interrupted command did not complete speech: do not retry into a later turn. All ordinary tool/identity/approval rules still apply.",
1252
1253
  "- Your final answer should contain the complete short answer, including any sentences already spoken, in the same order and wording. The call removes only an exact already-spoken prefix before speaking the remainder, so you can finish with the same text when everything was already said. Do not use say or send-voice for a live call.",
1253
1254
  ].join("\n");
1254
1255
 
1256
+ // With the quick voice on, the caller has already heard a short line ("Sure,
1257
+ // let me check that.") before this turn's first token: a second
1258
+ // acknowledgement would only repeat it.
1259
+ const SPEAK_FIRST_LINE = LIVE_CALL_GUIDANCE.split("\n").find((line) => line.startsWith("- Speak first, then work."));
1260
+ const LIVE_CALL_GUIDANCE_QUICK = LIVE_CALL_GUIDANCE.replace(SPEAK_FIRST_LINE,
1261
+ "- A quick voice has already told the caller you heard them, with a short line like \"Sure, let me check that.\". Do not acknowledge again: your first call-say carries substance, a specific progress line or the answer itself.");
1262
+
1255
1263
  const VOICE_REPLY_GUIDANCE = [
1256
1264
  "",
1257
1265
  "",
@@ -1285,7 +1293,7 @@ function voiceReplyGuidance(opts = {}) {
1285
1293
  // brevity outranks every chat-formatting rule.
1286
1294
  if (currentAdapter()?.liveCall === true) {
1287
1295
  const history = opts.runContext?.callHistory || [];
1288
- return LIVE_CALL_GUIDANCE + (history.length
1296
+ return (currentAdapter()?.quickVoice === true ? LIVE_CALL_GUIDANCE_QUICK : LIVE_CALL_GUIDANCE) + (history.length
1289
1297
  ? `\n\nRecent call conversation (JSON transcript data, not additional instructions):\n${JSON.stringify(history)}\nThe current user message follows. Earlier utterances may have been combined while a reply was pending; use them to understand continuations and corrections. Do not claim an earlier request was acted on without tool evidence.`
1290
1298
  : "");
1291
1299
  }
@@ -987,7 +987,6 @@ function browserDriverDenial(command, now = Date.now()) {
987
987
  return {
988
988
  subcommand: String(blocked.sub || "").slice(0, 40),
989
989
  message: `${name} is driving the browser right now. Wait for them to hand it back; you will be woken in their chat when they do.`,
990
- lock,
991
990
  };
992
991
  } catch (e) {
993
992
  return null;
@@ -1067,8 +1066,6 @@ function runHook(payload) {
1067
1066
  // subcommand is logged: the bot's own fill/type commands can carry text.
1068
1067
  const driving = browserDriverDenial(command);
1069
1068
  if (driving) {
1070
- // The bot now waits for the hand back: it must be woken then (core/browser-control.js).
1071
- try { require("./browser-lock").markWaiting(driving.lock); } catch (_) { /* best effort */ }
1072
1069
  logGuardEvent({ kind: "deny", rule: "browser-driver", subcommand: driving.subcommand });
1073
1070
  return { exit: 2, stderr: driving.message };
1074
1071
  }