@broberg/lens-daemon 0.1.39 → 0.1.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@broberg/lens-daemon",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "The Lens module of the cardmem daemon (flows, critic, coverage, capture sessions, the /lens/* routes and the cardmem-lens MCP) as a package. The daemon mounts createLensRouter(deps) + createLensMcpRouter(deps). Bun runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/browser.ts CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { bindProfileChannel, claimProfile, killOwnBrowsersOn, profileDir, type ProfileChannel } from './profiles.ts';
14
14
  import { sweepChromeClones, trackChromeLaunch } from './chrome-clones.ts';
15
- import { launchWithRepair, type BrowserRepair } from './ensure-chromium.ts';
15
+ import { launchWithRepair, repairInFlight, type BrowserRepair } from './ensure-chromium.ts';
16
16
  import {
17
17
  chromium,
18
18
  webkit,
@@ -119,6 +119,20 @@ export async function getBrowser(engine: LensEngine = 'chromium'): Promise<Brows
119
119
  // deadline; on timeout, discard the wedged browser and retry once fresh.
120
120
  const WEDGE_TIMEOUT_MS = 20_000;
121
121
 
122
+ /** F010.1 — the wedge deadline on a warm launch, except while that engine's
123
+ * browser is being reinstalled: then the repair is waited for (it has its own
124
+ * bound) and the deadline applies again after it. Exported for test. */
125
+ export async function launchDeadline<T>(p: Promise<T>, engine: LensEngine, ms: number): Promise<T> {
126
+ try {
127
+ return await withDeadline(p, ms, 'browser launch');
128
+ } catch (err) {
129
+ const repair = repairInFlight(engine);
130
+ if (!repair || !(err instanceof Error && err.message.includes('lens browser wedged'))) throw err;
131
+ await repair;
132
+ return withDeadline(p, ms, 'browser launch');
133
+ }
134
+ }
135
+
122
136
  function withDeadline<T>(p: Promise<T>, ms: number, what: string): Promise<T> {
123
137
  return new Promise<T>((resolve, reject) => {
124
138
  const t = setTimeout(
@@ -546,7 +560,7 @@ export async function withPage<T>(
546
560
  }
547
561
 
548
562
  const makeContext = async (): Promise<BrowserContext> => {
549
- const browser = await withDeadline(getBrowser(engine), WEDGE_TIMEOUT_MS, 'browser launch');
563
+ const browser = await launchDeadline(getBrowser(engine), engine, WEDGE_TIMEOUT_MS);
550
564
  return withDeadline(
551
565
  browser.newContext(contextOptions(opts)),
552
566
  WEDGE_TIMEOUT_MS,
@@ -148,6 +148,8 @@ export function isMissingBrowserError(err: unknown): boolean {
148
148
 
149
149
  type Installer = (names: string[]) => Promise<{ ok: boolean; error?: string }>;
150
150
 
151
+ const INSTALL_TIMEOUT_MS = 10 * 60_000;
152
+
151
153
  const spawnInstaller: Installer = (names) =>
152
154
  new Promise((resolve) => {
153
155
  let cliPath: string;
@@ -159,8 +161,13 @@ const spawnInstaller: Installer = (names) =>
159
161
  }
160
162
  console.log(`[lens] browser missing — installing ${names.join(', ')} via ${cliPath} …`);
161
163
  const child = spawn(process.execPath, [cliPath, 'install', ...names], { stdio: 'inherit', env: process.env });
162
- child.on('error', (err) => resolve({ ok: false, error: err.message }));
163
- child.on('exit', (code) => resolve(code === 0 ? { ok: true } : { ok: false, error: `playwright install exited ${code}` }));
164
+ // A download that stalls must not hold every run waiting on it forever.
165
+ const timer = setTimeout(() => {
166
+ child.kill('SIGKILL');
167
+ resolve({ ok: false, error: `playwright install did not finish within ${INSTALL_TIMEOUT_MS / 60_000} min` });
168
+ }, INSTALL_TIMEOUT_MS);
169
+ child.on('error', (err) => { clearTimeout(timer); resolve({ ok: false, error: err.message }); });
170
+ child.on('exit', (code) => { clearTimeout(timer); resolve(code === 0 ? { ok: true } : { ok: false, error: `playwright install exited ${code}` }); });
164
171
  });
165
172
 
166
173
  let installer: Installer = spawnInstaller;
@@ -198,16 +205,31 @@ export async function launchWithRepair<T>(
198
205
  } catch (err) {
199
206
  if (!isMissingBrowserError(err)) throw err;
200
207
  const t0 = Date.now();
201
- const inst = await installBrowsers(engine);
202
- if (!inst.ok) {
203
- throw new Error(`${engine} browser is missing and could not be installed: ${inst.error ?? 'unknown'} — original: ${(err as Error).message.split('\n')[0]}`);
204
- }
205
- const out = await launch();
206
- onRepaired?.({ engine, ms: Date.now() - t0 });
207
- return out;
208
+ const repair = (async () => {
209
+ const inst = await installBrowsers(engine);
210
+ if (!inst.ok) {
211
+ throw new Error(`${engine} browser is missing and could not be installed: ${inst.error ?? 'unknown'} — original: ${(err as Error).message.split('\n')[0]}`);
212
+ }
213
+ const out = await launch();
214
+ onRepaired?.({ engine, ms: Date.now() - t0 });
215
+ return out;
216
+ })();
217
+ const settled = repair.then(() => {}, () => {});
218
+ repairsInFlight.set(engine, settled);
219
+ void settled.then(() => { if (repairsInFlight.get(engine) === settled) repairsInFlight.delete(engine); });
220
+ return repair;
208
221
  }
209
222
  }
210
223
 
224
+ // F010.1 — measured live 25 Oct: the warm launch sits under a 20 s "wedged"
225
+ // deadline, and a firefox download takes longer, so the run failed while the
226
+ // repair was still installing. A caller's deadline asks this first: a repair in
227
+ // flight (install, bounded by INSTALL_TIMEOUT_MS, then one relaunch) is waited for.
228
+ const repairsInFlight = new Map<BrowserEngine, Promise<void>>();
229
+ export function repairInFlight(engine: BrowserEngine): Promise<void> | undefined {
230
+ return repairsInFlight.get(engine);
231
+ }
232
+
211
233
  /** For /health: does each engine's browser binary exist where this daemon looks?
212
234
  * chromium is reported for both the full build and the headless shell, because
213
235
  * the warm headless launch needs the shell (the 25 Oct gap boot never checked). */
package/src/flow-store.ts CHANGED
@@ -183,6 +183,12 @@ export async function recordFlowRun(
183
183
  // F009.128 — always written when the run reported it, empty included.
184
184
  ...(result.downloads ? { downloads: storedDownloads } : {}),
185
185
  ...(networkShapes ? { network_shapes: networkShapes } : {}),
186
+ // F009.137 / F010.1 / F010.7 — produced in flow.ts and, until measured live on
187
+ // 25 Oct (run 1a337c03: a mail was consumed, the report said nothing), written
188
+ // nowhere. Counts and engines only; never a code or a mail.
189
+ ...(result.mail ? { mail: result.mail } : {}),
190
+ ...(result.browser_repaired ? { browser_repaired: result.browser_repaired } : {}),
191
+ ...(result.chrome_clones_removed ? { chrome_clones_removed: result.chrome_clones_removed } : {}),
186
192
  }),
187
193
  };
188
194