@ateam-ai-mobile/expo 0.1.21 → 0.1.22

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 (33) hide show
  1. package/dist/aiLive/provider.d.mts +10 -4
  2. package/dist/aiLive/provider.d.ts +10 -4
  3. package/dist/aiLive/provider.js +119 -43
  4. package/dist/aiLive/provider.js.map +1 -1
  5. package/dist/aiLive/provider.mjs +5 -3
  6. package/dist/bootstrap.js +120 -46
  7. package/dist/bootstrap.js.map +1 -1
  8. package/dist/bootstrap.mjs +4 -4
  9. package/dist/capabilities/index.js +19 -1
  10. package/dist/capabilities/index.js.map +1 -1
  11. package/dist/capabilities/index.mjs +21 -3
  12. package/dist/capabilities/index.mjs.map +1 -1
  13. package/dist/{chunk-BAZFMTCC.mjs → chunk-4HJ5FO3S.mjs} +2 -2
  14. package/dist/{chunk-C62NQA4F.mjs → chunk-GGEFDRQQ.mjs} +2 -2
  15. package/dist/chunk-GGEFDRQQ.mjs.map +1 -0
  16. package/dist/{chunk-OEX4OKYQ.mjs → chunk-NH3CHSLR.mjs} +2 -2
  17. package/dist/{chunk-JUBLG3PN.mjs → chunk-PV2EHH2P.mjs} +119 -44
  18. package/dist/chunk-PV2EHH2P.mjs.map +1 -0
  19. package/dist/device/createDeviceProvider.mjs +3 -3
  20. package/dist/device-bridge/index.js +1 -1
  21. package/dist/device-bridge/index.js.map +1 -1
  22. package/dist/device-bridge/index.mjs +2 -2
  23. package/dist/push/silentSync.js.map +1 -1
  24. package/dist/push/silentSync.mjs +1 -1
  25. package/package.json +4 -4
  26. package/src/aiLive/provider.ts +138 -49
  27. package/src/aiLive/streamProtocol.ts +2 -2
  28. package/src/capabilities/store.ts +21 -1
  29. package/src/device-bridge/capabilities.ts +2 -1
  30. package/dist/chunk-C62NQA4F.mjs.map +0 -1
  31. package/dist/chunk-JUBLG3PN.mjs.map +0 -1
  32. /package/dist/{chunk-BAZFMTCC.mjs.map → chunk-4HJ5FO3S.mjs.map} +0 -0
  33. /package/dist/{chunk-OEX4OKYQ.mjs.map → chunk-NH3CHSLR.mjs.map} +0 -0
@@ -3,7 +3,8 @@ import {
3
3
  } from "./chunk-WQK6PUBN.mjs";
4
4
 
5
5
  // src/aiLive/provider.ts
6
- import { getCoreWsUrl, getTenant, getToken, mcpToolCall, unwrapMcpResult } from "@ateam-ai-mobile/core";
6
+ import { getCoreWsUrl, getTenant, getToken } from "@ateam-ai-mobile/core";
7
+ import { CONSENT_TAKEN_BACK } from "@ateam-ai-mobile/plugin-runtime";
7
8
 
8
9
  // src/aiLive/streamProtocol.ts
9
10
  var STREAM_PATH = "/ws/stream";
@@ -32,8 +33,8 @@ function encodeFrame({ seq, tCapture, payload }) {
32
33
  out.set(payload, HEADER_BYTES);
33
34
  return out;
34
35
  }
35
- function openMessage(kind) {
36
- return JSON.stringify({ type: "open", kind });
36
+ function openMessage(kind, params) {
37
+ return JSON.stringify(params ? { type: "open", kind, params } : { type: "open", kind });
37
38
  }
38
39
  var CLOSE_MESSAGE = JSON.stringify({ type: "close" });
39
40
  function parseServerEvent(text) {
@@ -81,22 +82,14 @@ function base64ToBytes(b64) {
81
82
  }
82
83
 
83
84
  // src/aiLive/provider.ts
84
- var PHONE_SLICE_USD = 0.5;
85
85
  var LIVE_KIND = "ai-models.live";
86
86
  var FRESH_MS = 3e3;
87
87
  var MAX_BUFFERED = 512 * 1024;
88
88
  var DEFAULT_CAMERA = { fps: 1, maxPx: 768 };
89
- var SESSION_UP = /* @__PURE__ */ new Set(["starting", "running", "degraded"]);
90
- async function defaultCallCp(method, params = {}) {
91
- const res = await mcpToolCall("cp.ai_models_api", { method, params });
92
- if (res?.error) throw new Error(res.error.message || "AI Models refused the request");
93
- const out = unwrapMcpResult(res?.result);
94
- if (res?.result?.isError) throw new Error(typeof out === "string" ? out : out?.message || out?.error || "AI Models refused the request");
95
- if (out && typeof out === "object" && out.ok === false) throw new Error(out.message || out.error || "AI Models refused the request");
96
- return out;
97
- }
89
+ var OPEN_TIMEOUT_MS = 2e4;
90
+ var CLOSE_WAIT_MS = 3e3;
91
+ var isEpisode = (d) => !!d && typeof d === "object" && typeof d.type === "string" && typeof d.class === "string" && typeof d.episodeId === "string" && typeof d.actorId === "string" && d.actorId.length > 0 && Number.isFinite(d.at);
98
92
  var defaultAiLiveDeps = () => ({
99
- callCp: defaultCallCp,
100
93
  WebSocketImpl: globalThis.WebSocket,
101
94
  wsBase: getCoreWsUrl,
102
95
  token: getToken,
@@ -104,7 +97,9 @@ var defaultAiLiveDeps = () => ({
104
97
  jpegById,
105
98
  now: Date.now,
106
99
  setInterval: (fn, ms) => setInterval(fn, ms),
107
- clearInterval: (h) => clearInterval(h)
100
+ clearInterval: (h) => clearInterval(h),
101
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
102
+ clearTimeout: (h) => clearTimeout(h)
108
103
  });
109
104
  var idle = () => ({ state: "idle", camera: null, sent: 0, skipped: 0, lastEpisode: null, lastError: null });
110
105
  function createAiLiveProvider(overrides = {}) {
@@ -114,7 +109,11 @@ function createAiLiveProvider(overrides = {}) {
114
109
  let ws = null;
115
110
  let loop = null;
116
111
  let visual = null;
117
- let startedSession = false;
112
+ let allowed = () => false;
113
+ let stopReason = null;
114
+ let tearingDown = null;
115
+ const waitingToStart = /* @__PURE__ */ new Map();
116
+ const closedOf = /* @__PURE__ */ new WeakMap();
118
117
  let closingOnPurpose = false;
119
118
  let seq = 0;
120
119
  let busy = false;
@@ -167,6 +166,12 @@ function createAiLiveProvider(overrides = {}) {
167
166
  const g = gen;
168
167
  const sock = ws;
169
168
  const current = () => g === gen && ws === sock && sock?.readyState === 1;
169
+ const consentGone = () => {
170
+ if (allowed()) return false;
171
+ if (g === gen) void teardown("stopped", CONSENT_TAKEN_BACK);
172
+ return true;
173
+ };
174
+ if (consentGone()) return;
170
175
  if (busy || !current()) {
171
176
  set({ skipped: snap.skipped + 1 });
172
177
  return;
@@ -187,7 +192,7 @@ function createAiLiveProvider(overrides = {}) {
187
192
  return;
188
193
  }
189
194
  const b64 = await deps.jpegById(id, camera.maxPx, 0.7);
190
- if (!current()) return;
195
+ if (!current() || consentGone()) return;
191
196
  const frame = encodeFrame({ seq: mySeq, tCapture: t, payload: base64ToBytes(b64) });
192
197
  sock.send(frame.buffer);
193
198
  lastFrameId = id;
@@ -211,22 +216,42 @@ function createAiLiveProvider(overrides = {}) {
211
216
  const sock = new deps.WebSocketImpl(url, [SUBPROTOCOL, `adas.bearer.${token}`]);
212
217
  ws = sock;
213
218
  sock.binaryType = "arraybuffer";
219
+ let markClosed = () => {
220
+ };
221
+ closedOf.set(sock, new Promise((r) => {
222
+ markClosed = r;
223
+ }));
224
+ const gaveUp = deps.setTimeout(() => {
225
+ if (opened) return;
226
+ reject(new Error("Core did not answer in time. Try again."));
227
+ try {
228
+ sock.close();
229
+ } catch {
230
+ }
231
+ }, OPEN_TIMEOUT_MS);
214
232
  sock.onopen = () => {
215
233
  try {
216
- sock.send(openMessage(LIVE_KIND));
234
+ sock.send(openMessage(LIVE_KIND, { session: "own", source: "phone" }));
217
235
  } catch (e) {
218
236
  reject(e);
219
237
  }
220
238
  };
221
239
  sock.onmessage = (m) => {
240
+ if (ws !== sock) return;
222
241
  const ev = parseServerEvent(m?.data);
223
242
  if (!ev) return;
243
+ if (ev.event === "session" && !opened) {
244
+ const c = ev.data?.camera;
245
+ camera = c && Number(c.fps) > 0 && Number(c.maxPx) > 0 ? { fps: Number(c.fps), maxPx: Number(c.maxPx) } : DEFAULT_CAMERA;
246
+ return;
247
+ }
224
248
  if (ev.event === "opened" && !opened) {
225
249
  opened = true;
250
+ deps.clearTimeout(gaveUp);
226
251
  resolve();
227
252
  return;
228
253
  }
229
- if (ev.event === "episode" && ev.data && typeof ev.data === "object") {
254
+ if (ev.event === "episode" && isEpisode(ev.data)) {
230
255
  const episode = ev.data;
231
256
  set({ lastEpisode: episode });
232
257
  for (const cb of owner && episodeCbs.get(owner) || []) {
@@ -240,8 +265,10 @@ function createAiLiveProvider(overrides = {}) {
240
265
  sock.onerror = () => {
241
266
  };
242
267
  sock.onclose = (c) => {
268
+ markClosed();
243
269
  const why = describeClose(Number(c?.code), c?.reason);
244
270
  if (!opened) {
271
+ deps.clearTimeout(gaveUp);
245
272
  reject(new Error(why));
246
273
  return;
247
274
  }
@@ -250,9 +277,20 @@ function createAiLiveProvider(overrides = {}) {
250
277
  };
251
278
  });
252
279
  }
253
- async function teardown(state, lastError) {
254
- const hadSession = startedSession;
255
- startedSession = false;
280
+ function teardown(state, lastError) {
281
+ if (tearingDown) return tearingDown;
282
+ const p = teardownNow(state, lastError);
283
+ tearingDown = p;
284
+ void p.then(() => {
285
+ }, () => {
286
+ }).then(() => {
287
+ if (tearingDown === p) tearingDown = null;
288
+ });
289
+ return p;
290
+ }
291
+ async function teardownNow(state, lastError) {
292
+ stopReason = lastError;
293
+ allowed = () => false;
256
294
  gen++;
257
295
  stopTimers();
258
296
  const sock = ws;
@@ -269,21 +307,40 @@ function createAiLiveProvider(overrides = {}) {
269
307
  closingOnPurpose = false;
270
308
  const v = visual;
271
309
  visual = null;
272
- try {
273
- await v?.stop?.();
274
- } catch {
275
- }
276
- if (hadSession) {
277
- try {
278
- await deps.callCp("liveStop");
279
- } catch {
280
- }
281
- }
310
+ let gaveUp = null;
311
+ const closed = sock && closedOf.get(sock);
312
+ await Promise.all([
313
+ (async () => {
314
+ try {
315
+ await v?.stop?.();
316
+ } catch {
317
+ }
318
+ })(),
319
+ closed ? Promise.race([closed, new Promise((r) => {
320
+ gaveUp = deps.setTimeout(r, CLOSE_WAIT_MS);
321
+ })]) : Promise.resolve()
322
+ ]);
323
+ if (gaveUp != null) deps.clearTimeout(gaveUp);
282
324
  owner = null;
283
325
  set({ state, lastError });
284
326
  }
285
327
  return {
286
328
  async start(opts, who, ctx) {
329
+ if (tearingDown) {
330
+ const wait = { cancelled: false };
331
+ waitingToStart.set(who, wait);
332
+ try {
333
+ while (tearingDown) {
334
+ try {
335
+ await tearingDown;
336
+ } catch {
337
+ }
338
+ }
339
+ } finally {
340
+ if (waitingToStart.get(who) === wait) waitingToStart.delete(who);
341
+ }
342
+ if (wait.cancelled) throw Object.assign(new Error("aiLive: stopped before it started."), { code: "STOPPED" });
343
+ }
287
344
  if (owner && owner !== who) throw Object.assign(new Error("aiLive: another screen is sending to AI right now. Close it first."), { code: "CONFLICT" });
288
345
  if (owner === who) return snap;
289
346
  const cam = opts?.camera === "front" ? "front" : "back";
@@ -291,35 +348,52 @@ function createAiLiveProvider(overrides = {}) {
291
348
  owner = who;
292
349
  lastOwner = who;
293
350
  visual = ctx.visual;
351
+ allowed = typeof ctx.allowed === "function" ? ctx.allowed : () => false;
294
352
  gen++;
295
353
  seq = 0;
296
354
  lastFrameId = null;
297
355
  busy = false;
356
+ const g = gen;
357
+ const cameraOfScreen = ctx.visual;
298
358
  snap = { ...idle(), camera: cam };
299
359
  set({ state: "starting" });
360
+ const stillStarting = async () => {
361
+ if (g === gen && !allowed()) await teardown("stopped", CONSENT_TAKEN_BACK);
362
+ if (g !== gen) throw Object.assign(new Error(stopReason || "aiLive: stopped before it started sending."), { code: "STOPPED" });
363
+ };
300
364
  try {
301
- let session = (await deps.callCp("liveStatus"))?.session;
302
- if (!SESSION_UP.has(session?.state)) {
303
- session = (await deps.callCp("liveStart", { source: "phone", budgetSliceUsd: PHONE_SLICE_USD }))?.session;
304
- startedSession = true;
305
- }
306
- const c = session?.camera;
307
- camera = c && Number(c.fps) > 0 && Number(c.maxPx) > 0 ? { fps: Number(c.fps), maxPx: Number(c.maxPx) } : DEFAULT_CAMERA;
308
- await visual.start({ intent: "stream", facing: cam });
365
+ await stillStarting();
366
+ camera = DEFAULT_CAMERA;
367
+ if (g !== gen) throw Object.assign(new Error(stopReason || "aiLive: stopped before it started sending."), { code: "STOPPED" });
309
368
  await openSocket();
369
+ await stillStarting();
370
+ await cameraOfScreen.start({ intent: "stream", facing: cam });
371
+ if (g !== gen) {
372
+ try {
373
+ await cameraOfScreen.stop?.();
374
+ } catch {
375
+ }
376
+ }
377
+ await stillStarting();
378
+ if (g !== gen) throw Object.assign(new Error(stopReason || "aiLive: stopped before it started sending."), { code: "STOPPED" });
310
379
  loop = deps.setInterval(() => {
311
380
  void slot();
312
381
  }, Math.max(200, Math.round(1e3 / camera.fps)));
313
382
  set({ state: "sending" });
314
383
  return snap;
315
384
  } catch (e) {
385
+ if (g !== gen) {
386
+ throw e?.code === "STOPPED" ? e : Object.assign(new Error(stopReason || "aiLive: stopped before it started sending."), { code: "STOPPED" });
387
+ }
316
388
  await teardown("error", String(e?.message || e));
317
389
  throw e;
318
390
  }
319
391
  },
320
- async stop(who) {
392
+ async stop(who, why) {
393
+ const waiting = waitingToStart.get(who);
394
+ if (waiting) waiting.cancelled = true;
321
395
  if (owner !== who) return;
322
- await teardown("stopped", null);
396
+ await teardown("stopped", why ?? null);
323
397
  },
324
398
  getSnapshot: (who) => lastOwner === who ? snap : idle(),
325
399
  onEpisode: (cb, who) => add(episodeCbs, who, cb),
@@ -343,9 +417,10 @@ function createAiLiveProvider(overrides = {}) {
343
417
  }
344
418
 
345
419
  export {
346
- PHONE_SLICE_USD,
347
420
  LIVE_KIND,
421
+ OPEN_TIMEOUT_MS,
422
+ CLOSE_WAIT_MS,
348
423
  defaultAiLiveDeps,
349
424
  createAiLiveProvider
350
425
  };
351
- //# sourceMappingURL=chunk-JUBLG3PN.mjs.map
426
+ //# sourceMappingURL=chunk-PV2EHH2P.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/aiLive/provider.ts","../src/aiLive/streamProtocol.ts"],"sourcesContent":["/**\n * The AI live provider: this phone's camera → the workspace's live session → the chosen AI,\n * and what the AI saw back to the screen. Installed by the host once at startup:\n * import { createAiLiveProvider } from '@ateam-ai-mobile/expo/ai-live';\n * setAiLiveProvider(createAiLiveProvider());\n * A plugin reaches it only through props.aiLive,\n * whose gate (plugin-runtime aiLiveHost) has already checked the two-sided consent. The gate\n * passes that check as ctx.allowed, and it is asked again right before every picture: once the\n * person takes the yes back, nothing more is sent and the session stops (GPT review 2026-10-30).\n *\n * One camera, one sender: while one screen sends, another's start() is refused by name.\n *\n * ONE CONNECTION IS THE SESSION (owner direction 2026-10-30). start():\n * 1. opens Core's stream endpoint (/ws/stream, kind \"ai-models.live\", the login in the\n * subprotocol) asking for a session of its own (params.session \"own\"). That one round trip\n * IS starting the person's live session: Core starts it with its own $0.50 cap, refuses in\n * plain words when the person already has one running (never joins), and says how to send\n * ({\"event\":\"session\"}: the rate and size the setup was tested at) before \"opened\";\n * 2. turns on the screen's held camera on the requested side;\n * 3. sends one picture per slot at that rate and size, from the newest fresh frame, as a JPEG\n * made by native (jpegById). A slot with no fresh frame, or while the socket is still busy,\n * sends nothing and still takes its sequence number, so Core reports it as a gap;\n * 4. {\"event\":\"episode\"} from Core → the screen's onEpisode callbacks.\n * stop() closes the connection and the camera; Core ends the session when the connection ends,\n * whatever ended it (a stop, the screen closing, the app killed, the network lost).\n */\nimport { getCoreWsUrl, getTenant, getToken } from '@ateam-ai-mobile/core';\nimport { CONSENT_TAKEN_BACK } from '@ateam-ai-mobile/plugin-runtime';\nimport type { AiLiveEpisode, AiLiveProvider, AiLiveSnapshot, AiLiveStartOptions, DeviceState, Subscription } from '@ateam-ai-mobile/plugin-runtime';\nimport { jpegById as nativeJpegById } from '../vision';\nimport { CLOSE_MESSAGE, STREAM_PATH, SUBPROTOCOL, base64ToBytes, describeClose, encodeFrame, openMessage, parseServerEvent } from './streamProtocol';\n\nexport const LIVE_KIND = 'ai-models.live';\n/** A frame older than this is not \"what the camera sees now\" and is not sent. */\nconst FRESH_MS = 3000;\n/** Bytes still waiting in the socket above which a slot is skipped rather than queued. */\nconst MAX_BUFFERED = 512 * 1024;\nconst DEFAULT_CAMERA = { fps: 1, maxPx: 768 };\n\ntype Visual = DeviceState['visual'];\n\nexport interface AiLiveDeps {\n WebSocketImpl: any;\n wsBase(): string;\n token(): string | null;\n tenant(): string | null;\n jpegById(id: string, maxSide: number, quality: number): Promise<string>;\n now(): number;\n setInterval(fn: () => void, ms: number): any;\n clearInterval(handle: any): void;\n setTimeout(fn: () => void, ms: number): any;\n clearTimeout(handle: any): void;\n}\n\n/** Core answers \"opened\" in one round trip (it starts the session first). Past this, the start gives up\n * and closes the connection, so a start can never hang a screen's queue. */\nexport const OPEN_TIMEOUT_MS = 20_000;\n/** A stop waits this long, at most, for its connection to finish closing: Core ends the session when it\n * sees the close, so a restart right after a stop must not open the next one before (Codex on #53).\n * Past this the transport is taken as broken, and Core's own 60 s rule ends that session. */\nexport const CLOSE_WAIT_MS = 3_000;\n\nconst isEpisode = (d: any): d is AiLiveEpisode =>\n !!d && typeof d === 'object' && typeof d.type === 'string' && typeof d.class === 'string' &&\n typeof d.episodeId === 'string' && typeof d.actorId === 'string' && d.actorId.length > 0 && Number.isFinite(d.at);\n\nexport const defaultAiLiveDeps = (): AiLiveDeps => ({\n WebSocketImpl: (globalThis as any).WebSocket,\n wsBase: getCoreWsUrl,\n token: getToken,\n tenant: getTenant,\n jpegById: nativeJpegById,\n now: Date.now,\n setInterval: (fn, ms) => setInterval(fn, ms),\n clearInterval: (h) => clearInterval(h),\n setTimeout: (fn, ms) => setTimeout(fn, ms),\n clearTimeout: (h) => clearTimeout(h),\n});\n\nconst idle = (): AiLiveSnapshot => ({ state: 'idle', camera: null, sent: 0, skipped: 0, lastEpisode: null, lastError: null });\n\nexport function createAiLiveProvider(overrides: Partial<AiLiveDeps> = {}): AiLiveProvider {\n const deps: AiLiveDeps = { ...defaultAiLiveDeps(), ...overrides };\n let owner: string | null = null; // the screen sending now\n let snap: AiLiveSnapshot = idle();\n let ws: any = null;\n let loop: any = null;\n let visual: Visual | null = null;\n let allowed: () => boolean = () => false; // the two-sided consent, asked before every picture\n let stopReason: string | null = null; // why the last teardown happened, known before it finishes\n // A teardown still finishing (the camera, the connection): a start waits for it, so a quick restart neither\n // reads the old session as its own nor is cleared by it afterwards (Codex on #53). Every teardown is\n // tracked, the ones the provider starts itself (consent gone, the connection dropped) included.\n let tearingDown: Promise<void> | null = null;\n // Screens whose start is waiting for that teardown, before they own anything: a stop meanwhile cancels\n // the start (it would otherwise go ahead after the screen had stopped or closed; Codex on #53).\n const waitingToStart = new Map<string, { cancelled: boolean }>();\n /** Each connection's \"it has closed\": a stop waits for it (CLOSE_WAIT_MS at most). */\n const closedOf = new WeakMap<object, Promise<void>>();\n let closingOnPurpose = false;\n let seq = 0;\n let busy = false;\n // Which session a slot belongs to. A slot awaits the camera and the JPEG maker; when the screen\n // stops (or another starts) meanwhile, that slot's picture must go nowhere, not into the next one.\n let gen = 0;\n let lastFrameId: string | null = null;\n let camera = DEFAULT_CAMERA;\n let lastOwner: string | null = null; // whose the current snapshot is (it outlives a stop)\n // Each screen hears only its own session: callbacks are kept per holder.\n const episodeCbs = new Map<string, Set<(ev: AiLiveEpisode) => void>>();\n const snapCbs = new Map<string, Set<(s: AiLiveSnapshot) => void>>();\n const sendingCbs = new Set<() => void>();\n let wasSending = false;\n const add = <T>(m: Map<string, Set<T>>, who: string, cb: T): Subscription => {\n let set = m.get(who);\n if (!set) { set = new Set(); m.set(who, set); }\n set.add(cb);\n return { unsubscribe: () => { set!.delete(cb); } };\n };\n\n const set = (patch: Partial<AiLiveSnapshot>) => {\n snap = { ...snap, ...patch };\n for (const cb of (lastOwner && snapCbs.get(lastOwner)) || []) { try { cb(snap); } catch { /* a listener's error is its own */ } }\n const sending = snap.state === 'starting' || snap.state === 'sending';\n if (sending !== wasSending) { wasSending = sending; for (const cb of sendingCbs) { try { cb(); } catch { /* idem */ } } }\n };\n\n function stopTimers() {\n if (loop != null) { deps.clearInterval(loop); loop = null; }\n }\n\n async function slot() {\n const mySeq = seq++; // every slot takes a number, sent or not\n const g = gen;\n const sock = ws;\n const current = () => g === gen && ws === sock && sock?.readyState === 1;\n const consentGone = () => { if (allowed()) return false; if (g === gen) void teardown('stopped', CONSENT_TAKEN_BACK); return true; };\n if (consentGone()) return;\n if (busy || !current()) { set({ skipped: snap.skipped + 1 }); return; }\n if ((sock.bufferedAmount || 0) > MAX_BUFFERED) { set({ skipped: snap.skipped + 1 }); return; }\n busy = true;\n try {\n const v = await visual!.getSnapshot();\n if (!current()) return;\n const f: any = v?.latest;\n const id = f?.handle?.id;\n const t = Number(f?.timestamp);\n if (!id || id === lastFrameId || !(deps.now() - t < FRESH_MS)) { set({ skipped: snap.skipped + 1 }); return; }\n const b64 = await deps.jpegById(id, camera.maxPx, 0.7);\n if (!current() || consentGone()) return;\n const frame = encodeFrame({ seq: mySeq, tCapture: t, payload: base64ToBytes(b64) });\n sock.send(frame.buffer);\n lastFrameId = id;\n set({ sent: snap.sent + 1 });\n } catch (e: any) {\n if (g === gen) set({ skipped: snap.skipped + 1, lastError: String(e?.message || e) });\n } finally {\n if (g === gen) busy = false;\n }\n }\n\n function openSocket(): Promise<void> {\n return new Promise((resolve, reject) => {\n const token = deps.token();\n const tenant = deps.tenant();\n if (!token || !tenant) { reject(new Error('not signed in to a workspace')); return; }\n const url = `${deps.wsBase()}${STREAM_PATH}?tenant=${encodeURIComponent(tenant)}`;\n let opened = false;\n const sock = new deps.WebSocketImpl(url, [SUBPROTOCOL, `adas.bearer.${token}`]);\n ws = sock;\n sock.binaryType = 'arraybuffer';\n let markClosed: () => void = () => {};\n closedOf.set(sock, new Promise<void>((r) => { markClosed = r; }));\n const gaveUp = deps.setTimeout(() => {\n if (opened) return;\n reject(new Error('Core did not answer in time. Try again.'));\n try { sock.close(); } catch { /* closing anyway */ }\n }, OPEN_TIMEOUT_MS);\n // Its own session: opening IS starting it, and Core ends it when this connection ends.\n sock.onopen = () => { try { sock.send(openMessage(LIVE_KIND, { session: 'own', source: 'phone' })); } catch (e) { reject(e); } };\n sock.onmessage = (m: any) => {\n // Only the current connection speaks for the current session. A message from a connection\n // already stopped or replaced (it can still be queued after a stop, a restart or a switch to\n // another screen) is dropped whole: its episode never reaches a newer screen, and its \"session\"\n // or \"opened\" never touches a newer connection (GPT code review v5).\n if (ws !== sock) return;\n const ev = parseServerEvent(m?.data);\n if (!ev) return;\n if (ev.event === 'session' && !opened) {\n const c = ev.data?.camera;\n camera = c && Number(c.fps) > 0 && Number(c.maxPx) > 0 ? { fps: Number(c.fps), maxPx: Number(c.maxPx) } : DEFAULT_CAMERA;\n return;\n }\n if (ev.event === 'opened' && !opened) { opened = true; deps.clearTimeout(gaveUp); resolve(); return; }\n // Only a whole episode reaches the screen: what happened, of which class, and whose camera saw it\n // (actorId: live sessions are per person). Anything less is dropped, never passed on half-made.\n if (ev.event === 'episode' && isEpisode(ev.data)) {\n const episode = ev.data;\n set({ lastEpisode: episode });\n for (const cb of (owner && episodeCbs.get(owner)) || []) { try { cb(episode); } catch { /* a listener's error is its own */ } }\n }\n };\n sock.onerror = () => { /* the close that follows says why */ };\n sock.onclose = (c: any) => {\n markClosed();\n const why = describeClose(Number(c?.code), c?.reason);\n if (!opened) { deps.clearTimeout(gaveUp); reject(new Error(why)); return; }\n if (closingOnPurpose || ws !== sock) return;\n void teardown('error', why);\n };\n });\n }\n\n function teardown(state: 'stopped' | 'error', lastError: string | null): Promise<void> {\n // One at a time: a second stop while one is finishing joins it (Codex on #53). Two at once could\n // finish out of order and the first, resuming late, would stop a session started in between.\n if (tearingDown) return tearingDown;\n const p = teardownNow(state, lastError);\n tearingDown = p;\n void p.then(() => {}, () => {}).then(() => { if (tearingDown === p) tearingDown = null; });\n return p;\n }\n\n // Closing the connection ends the session (Core); the camera is this phone's to stop.\n async function teardownNow(state: 'stopped' | 'error', lastError: string | null) {\n stopReason = lastError;\n allowed = () => false;\n gen++;\n stopTimers();\n const sock = ws;\n ws = null;\n closingOnPurpose = true;\n try { if (sock && sock.readyState === 1) sock.send(CLOSE_MESSAGE); } catch { /* closing anyway */ }\n try { sock?.close?.(); } catch { /* already closed */ }\n closingOnPurpose = false;\n const v = visual;\n visual = null;\n // The camera off, and the connection really closed (so Core has ended its session) before this stop\n // is done: a start right after it then never meets the old session (Codex on #53).\n let gaveUp: any = null;\n const closed = sock && closedOf.get(sock);\n await Promise.all([\n (async () => { try { await v?.stop?.(); } catch { /* the camera hold is released by the gate too */ } })(),\n closed ? Promise.race([closed, new Promise<void>((r) => { gaveUp = deps.setTimeout(r, CLOSE_WAIT_MS); })]) : Promise.resolve(),\n ]);\n if (gaveUp != null) deps.clearTimeout(gaveUp);\n owner = null;\n set({ state, lastError });\n }\n\n return {\n async start(opts: AiLiveStartOptions, who: string, ctx: { visual: Visual; allowed: () => boolean }) {\n if (tearingDown) {\n const wait = { cancelled: false };\n waitingToStart.set(who, wait);\n try {\n while (tearingDown) { try { await tearingDown; } catch { /* it stopped either way */ } }\n } finally {\n if (waitingToStart.get(who) === wait) waitingToStart.delete(who);\n }\n if (wait.cancelled) throw Object.assign(new Error('aiLive: stopped before it started.'), { code: 'STOPPED' });\n }\n if (owner && owner !== who) throw Object.assign(new Error('aiLive: another screen is sending to AI right now. Close it first.'), { code: 'CONFLICT' });\n if (owner === who) return snap;\n const cam = opts?.camera === 'front' ? 'front' : 'back';\n if (opts?.camera && opts.camera !== 'front' && opts.camera !== 'back') throw new Error(`aiLive: unknown camera \"${String(opts.camera)}\" — use \"front\" or \"back\"`);\n owner = who;\n lastOwner = who;\n visual = ctx.visual;\n allowed = typeof ctx.allowed === 'function' ? ctx.allowed : () => false;\n gen++; seq = 0; lastFrameId = null; busy = false;\n const g = gen;\n const cameraOfScreen = ctx.visual;\n snap = { ...idle(), camera: cam };\n set({ state: 'starting' });\n // Starting takes several awaits, and the screen may be stopped, or the yes taken back,\n // during any of them (Codex P1 on #53). After each: a stop already tore it down, so say why\n // and go no further; a yes taken back unannounced tears it down here.\n const stillStarting = async () => {\n if (g === gen && !allowed()) await teardown('stopped', CONSENT_TAKEN_BACK);\n if (g !== gen) throw Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n };\n try {\n await stillStarting();\n // One round trip: the connection starts the session (or Core refuses, in its own words), and\n // says how to send. Before the camera, so a refusal never turns the camera on.\n camera = DEFAULT_CAMERA;\n // Checked again with nothing in between: a stop that landed while the check above resolved has\n // already torn this start down, and opening now would start a session nobody closes (Codex on #53).\n if (g !== gen) throw Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n await openSocket();\n await stillStarting();\n await cameraOfScreen.start({ intent: 'stream', facing: cam } as any);\n if (g !== gen) { try { await cameraOfScreen.stop?.(); } catch { /* released by the gate too */ } }\n await stillStarting();\n // Again with nothing in between: a stop that landed while the last check resolved must not see\n // pictures start or the state flip back to \"sending\" (Codex on #53).\n if (g !== gen) throw Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n loop = deps.setInterval(() => { void slot(); }, Math.max(200, Math.round(1000 / camera.fps)));\n set({ state: 'sending' });\n return snap;\n } catch (e: any) {\n if (g !== gen) {\n // Already torn down by a stop: report why it stopped, not the closed socket.\n throw e?.code === 'STOPPED' ? e : Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n }\n await teardown('error', String(e?.message || e));\n throw e;\n }\n },\n async stop(who: string, why?: string) {\n const waiting = waitingToStart.get(who);\n if (waiting) waiting.cancelled = true;\n if (owner !== who) return;\n await teardown('stopped', why ?? null);\n },\n getSnapshot: (who: string) => (lastOwner === who ? snap : idle()),\n onEpisode: (cb, who) => add(episodeCbs, who, cb),\n subscribe: (cb, who) => add(snapCbs, who, cb),\n forget(who: string) {\n episodeCbs.delete(who);\n snapCbs.delete(who);\n if (lastOwner === who && owner === null) { lastOwner = null; snap = idle(); }\n },\n isSending: () => snap.state === 'starting' || snap.state === 'sending',\n subscribeSending(cb) {\n sendingCbs.add(cb);\n return { unsubscribe: () => { sendingCbs.delete(cb); } } as Subscription;\n },\n };\n}\n","/**\n * The wire protocol of Core's binary stream endpoint (/ws/stream), for the AI live sender.\n *\n * A PORT of Core's `packages/media-stream/src/index.js` (@ateam-ai/media-stream, not on npm\n * yet), limited to what a sender needs. The frame bytes are pinned by\n * tests/aiLiveStreamProtocol.test.mjs against the layout that file documents, so a drift is\n * a red test, not a silent mismatch. DEAD — replace with an import of @ateam-ai/media-stream\n * once Core publishes it.\n *\n * Frame envelope (bytes): 0 version (1) · 1 flags · 2-5 seq uint32 BE (one per sampling slot\n * of the SENDER, taken or skipped) · 6-13 tCapture float64 BE (ms since the epoch) · 14- payload.\n */\nexport const STREAM_PATH = '/ws/stream';\nexport const SUBPROTOCOL = 'adas.v1';\nexport const HEADER_BYTES = 14;\nconst ENVELOPE_VERSION = 1;\n\nexport const CLOSE = Object.freeze({\n AUTH_MISSING: 4001,\n AUTH_FAILED: 4002,\n NO_SUCH_WORKSPACE: 4003,\n NOT_ENTITLED: 4004,\n BAD_REQUEST: 4400,\n UNKNOWN_KIND: 4404,\n TIMEOUT: 4408,\n OVER_LIMIT: 4429,\n TOO_BIG: 1009,\n CONSUMER_FAILING: 1011,\n});\n\nexport function encodeFrame({ seq, tCapture, payload }: { seq: number; tCapture: number; payload: Uint8Array }): Uint8Array {\n const out = new Uint8Array(HEADER_BYTES + payload.length);\n const v = new DataView(out.buffer);\n v.setUint8(0, ENVELOPE_VERSION);\n v.setUint8(1, 0);\n v.setUint32(2, seq >>> 0, false);\n v.setFloat64(6, tCapture, false);\n out.set(payload, HEADER_BYTES);\n return out;\n}\n\nexport function openMessage(kind: string, params?: Record<string, unknown>): string {\n return JSON.stringify(params ? { type: 'open', kind, params } : { type: 'open', kind });\n}\n\nexport const CLOSE_MESSAGE = JSON.stringify({ type: 'close' });\n\n/** A server text frame → { event, data }, or null for anything that is not an event. */\nexport function parseServerEvent(text: unknown): { event: string; data: any } | null {\n if (typeof text !== 'string') return null;\n try {\n const m = JSON.parse(text);\n return m && typeof m.event === 'string' ? { event: m.event, data: m.data } : null;\n } catch {\n return null;\n }\n}\n\nconst WHY: Record<number, string> = {\n [CLOSE.AUTH_MISSING]: 'The login or the workspace was missing. Sign in again and try once more.',\n [CLOSE.AUTH_FAILED]: 'The login was refused. Sign in again and try once more.',\n [CLOSE.NO_SUCH_WORKSPACE]: 'This workspace was not found.',\n [CLOSE.NOT_ENTITLED]: 'This workspace may not stream right now.',\n [CLOSE.TIMEOUT]: 'The connection to the AI timed out.',\n [CLOSE.OVER_LIMIT]: 'Too many cameras are sending to this workspace. Stop another one first.',\n [CLOSE.TOO_BIG]: 'A picture was too big to send.',\n [CLOSE.CONSUMER_FAILING]: 'The AI stopped taking pictures. Check the live session in AI Models.',\n};\n\n/** A close as one plain sentence: the server's own words for a refused open. */\nexport function describeClose(code: number, reason?: string): string {\n if (code === CLOSE.BAD_REQUEST && reason) return reason;\n return WHY[code] || (reason ? reason : `The connection to the AI closed (${code}).`);\n}\n\nconst B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst B64_INDEX = (() => { const t = new Int16Array(256).fill(-1); for (let i = 0; i < 64; i++) t[B64.charCodeAt(i)] = i; return t; })();\n\n/** Base64 (the native module's JPEG) → bytes. Ignores padding and line breaks. */\nexport function base64ToBytes(b64: string): Uint8Array {\n const clean = b64.replace(/[^A-Za-z0-9+/]/g, '');\n const out = new Uint8Array(Math.floor((clean.length * 3) / 4));\n let o = 0, acc = 0, bits = 0;\n for (let i = 0; i < clean.length; i++) {\n acc = (acc << 6) | B64_INDEX[clean.charCodeAt(i)];\n bits += 6;\n if (bits >= 8) { bits -= 8; out[o++] = (acc >> bits) & 0xff; }\n }\n return o === out.length ? out : out.subarray(0, o);\n}\n"],"mappings":"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","names":["set"]}
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  clearAllPluginData,
3
3
  createDeviceProvider
4
- } from "../chunk-OEX4OKYQ.mjs";
4
+ } from "../chunk-NH3CHSLR.mjs";
5
5
  import "../chunk-4BMWVN25.mjs";
6
6
  import "../chunk-6LXF3JVX.mjs";
7
7
  import "../chunk-WQK6PUBN.mjs";
8
- import "../chunk-BAZFMTCC.mjs";
9
- import "../chunk-C62NQA4F.mjs";
8
+ import "../chunk-4HJ5FO3S.mjs";
9
+ import "../chunk-GGEFDRQQ.mjs";
10
10
  import "../chunk-XQSY6MHP.mjs";
11
11
  import "../chunk-KOXF2FZM.mjs";
12
12
  import "../chunk-7FWYEHN5.mjs";
@@ -1625,7 +1625,7 @@ function cameraCapability() {
1625
1625
  return {
1626
1626
  id: "camera",
1627
1627
  name: "Camera",
1628
- description: "Lets ada see through your camera to guide you \u2014 reading text and describing what's in front of you. Frames are analysed on the phone; nothing is stored or uploaded.",
1628
+ description: `Lets ada see through your camera to guide you \u2014 reading text and describing what's in front of you. Frames are analysed on the phone and nothing is stored; nothing is uploaded unless you also turn on "Camera pictures to AI".`,
1629
1629
  reason: "Powers visual guidance (Ada Guide): a live view with on-device text and scene understanding.",
1630
1630
  icon: "camera",
1631
1631
  requiresPermission: true,