@c4t4/heyamigo 0.9.11 → 0.9.12

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/dist/config.js CHANGED
@@ -44,6 +44,14 @@ const ConfigSchema = z.object({
44
44
  size: z.number().int().positive().default(5),
45
45
  })
46
46
  .default({ size: 5 }),
47
+ browser: z
48
+ .object({
49
+ // How many browser tasks can run in parallel on the shared
50
+ // Chrome. Each worker drives its own tab. Persistent agent
51
+ // session was dropped in Phase 4; every task is fresh.
52
+ maxWorkers: z.number().int().positive().default(3),
53
+ })
54
+ .default({ maxWorkers: 3 }),
47
55
  codex: z
48
56
  .object({
49
57
  // Optional model override. If unset, Codex uses its default. Passed
@@ -1,5 +1,3 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
- import { dirname, resolve } from 'path';
3
1
  import { getProvider } from '../ai/providers.js';
4
2
  import { config } from '../config.js';
5
3
  import fastq from 'fastq';
@@ -236,67 +234,19 @@ function truncate(s, n) {
236
234
  //
237
235
  // - Concurrency is 1. Serialized against itself because (a) the shared
238
236
  // Playwright MCP + Chrome is one physical resource, (b) the session below
239
- // is persistent and --resume doesn't allow concurrent resumes.
240
- // - One GLOBAL persistent session stored at storage/browser-session.json.
241
- // First browser task bootstraps fresh (captures sessionId). Subsequent
242
- // tasks spawn with --resume <sessionId>, so the browser Claude carries
243
- // memory of prior tasks across runs.
244
- // - Task description is added as a new user message to the persistent
245
- // session. The worker sees the accumulated history automatically.
246
- // Per-provider browser session storage. Each CLI's session ids are opaque
247
- // to the other, so swapping providers must not feed one's session id to
248
- // the other. Filename includes the provider name to keep them separate;
249
- // the legacy provider-less filename is auto-migrated to claude on read.
250
- function browserSessionFilePath(provider) {
251
- return resolve(process.cwd(), config.memory.dir, `browser-session-${provider}.json`);
252
- }
253
- function legacyBrowserSessionFilePath() {
254
- return resolve(process.cwd(), config.memory.dir, 'browser-session.json');
255
- }
256
- function loadBrowserSession(provider) {
257
- const path = browserSessionFilePath(provider);
258
- let source = path;
259
- if (!existsSync(path)) {
260
- const legacy = legacyBrowserSessionFilePath();
261
- if (provider === 'claude' && existsSync(legacy)) {
262
- // One-time migration: legacy file was implicitly claude.
263
- source = legacy;
264
- }
265
- else {
266
- return { sessionId: null, createdAt: 0, lastUsedAt: 0, resumeCount: 0 };
267
- }
268
- }
269
- try {
270
- const parsed = JSON.parse(readFileSync(source, 'utf-8'));
271
- return {
272
- sessionId: parsed.sessionId ?? null,
273
- createdAt: parsed.createdAt ?? 0,
274
- lastUsedAt: parsed.lastUsedAt ?? 0,
275
- resumeCount: parsed.resumeCount ?? 0,
276
- };
277
- }
278
- catch {
279
- return { sessionId: null, createdAt: 0, lastUsedAt: 0, resumeCount: 0 };
280
- }
281
- }
282
- function saveBrowserSession(provider, state) {
283
- const path = browserSessionFilePath(provider);
284
- mkdirSync(dirname(path), { recursive: true });
285
- writeFileSync(path, JSON.stringify(state, null, 2) + '\n', 'utf-8');
286
- }
287
- // Reset the browser session for the active provider. Callable from outside
288
- // if the session gets corrupted or we want a fresh start. Not wired into
289
- // any command yet.
290
- export function resetBrowserSession() {
291
- const provider = getProvider().name;
292
- saveBrowserSession(provider, {
293
- sessionId: null,
294
- createdAt: 0,
295
- lastUsedAt: 0,
296
- resumeCount: 0,
297
- });
298
- logger.info({ provider }, 'browser session reset');
299
- }
237
+ // is one physical resource.
238
+ // - Persistent agent session DROPPED in Phase 4 — multiple browser
239
+ // tasks now run concurrently, each in its own Chrome tab, each as
240
+ // a fresh agent. Cross-task agent memory was rarely load-bearing
241
+ // (the chat-track agent writes self-contained task descriptions).
242
+ // Per-task tab isolation is enforced by the prompt instructions
243
+ // below.
244
+ // Browser pool: multiple agents share one Chrome (the logged-in
245
+ // profile), each task opens its own tab. Persistent agent session is
246
+ // dropped every task is fresh, with self-contained instructions
247
+ // from the chat-track agent. The trade-off: no cross-task agent
248
+ // memory; the win: real parallelism.
249
+ const BROWSER_CONCURRENCY = Math.max(1, config.browser?.maxWorkers ?? 3);
300
250
  const browserQueue = fastq.promise(async (task) => {
301
251
  inProgress.set(task.id, task);
302
252
  try {
@@ -308,7 +258,7 @@ const browserQueue = fastq.promise(async (task) => {
308
258
  finally {
309
259
  inProgress.delete(task.id);
310
260
  }
311
- }, 1);
261
+ }, BROWSER_CONCURRENCY);
312
262
  export function enqueueBrowserTask(input) {
313
263
  const task = {
314
264
  ...input,
@@ -323,12 +273,15 @@ export function enqueueBrowserTask(input) {
323
273
  browserQueue.push(task).catch((err) => logger.error({ err, id: task.id }, 'browser queue push failed'));
324
274
  return task;
325
275
  }
326
- function buildBrowserPrompt(task, isResume) {
327
- // Framing tuned for the dedicated browser worker.
276
+ function buildBrowserPrompt(task) {
277
+ // Framing tuned for the dedicated browser worker. Each task is its
278
+ // own fresh agent run (no persistent session) — multiple browser
279
+ // tasks may be running in parallel on the same Chrome, each in its
280
+ // own tab.
328
281
  const lines = [
329
- isResume
330
- ? `You are the BROWSER WORKER. Another task just came in. You already have memory of prior browser tasks in this session — act on it accordingly. Use the shared Chrome at localhost:9222 via Playwright MCP (already logged into the owner's sessions like TikTok, Instagram, etc. — do NOT log out, do NOT start a new browser instance).`
331
- : `You are the BROWSER WORKER. You run in a persistent session dedicated to browser tasks for the owner. The chat already got its ack; your output IS the follow-up chat reply the owner is waiting for. Use the shared Chrome at localhost:9222 via Playwright MCP (already authenticated with the owner's sessionsTikTok, Instagram, etc. do NOT log out, do NOT launch a new browser).`,
282
+ `You are the BROWSER WORKER. The chat already got its ack; your output IS the follow-up chat reply the owner is waiting for. Use the shared Chrome at localhost:9222 via Playwright MCP (already authenticated with the owner's sessions — TikTok, Instagram, etc. — do NOT log out, do NOT launch a new browser).`,
283
+ ``,
284
+ `TAB OWNERSHIP: Other browser workers may be running concurrently on the SAME Chrome instance, each driving its own tab. Your FIRST action is to open a new tab for this task (browser_tabs with action=new). Operate ONLY on that tab for the rest of the task. Do NOT switch to or interact with tabs you didn't openthey belong to other workers. Close your tab when you finish.`,
332
285
  ``,
333
286
  `TASK:`,
334
287
  task.description,
@@ -373,25 +326,24 @@ function browserAddDirs() {
373
326
  }
374
327
  async function runBrowserTask(task) {
375
328
  const provider = getProvider();
376
- const session = loadBrowserSession(provider.name);
377
- const isResume = !!session.sessionId;
378
- const prompt = buildBrowserPrompt(task, isResume);
329
+ // Each task is fresh (Phase 4 browser parallelism). No persistent
330
+ // session would force serialization on concurrent tasks.
331
+ // Chat-track agent writes self-contained task descriptions, so the
332
+ // worker doesn't need cross-task agent memory.
333
+ const prompt = buildBrowserPrompt(task);
379
334
  const elapsedLog = () => `${Math.round((Date.now() - task.startedAt * 1000) / 1000)}s`;
380
335
  let reply;
381
- let returnedSessionId;
382
336
  try {
383
337
  const result = await provider.runTask({
384
338
  input: prompt,
385
339
  caller: 'browser-task',
386
340
  mode: 'auto',
387
341
  lane: 'async',
388
- includeSystemPrompt: !isResume,
342
+ includeSystemPrompt: true,
389
343
  addDirs: browserAddDirs(),
390
344
  allowedTools: task.allowedTools,
391
- sessionId: session.sessionId ?? undefined,
392
345
  });
393
346
  reply = result.reply;
394
- returnedSessionId = result.sessionId;
395
347
  }
396
348
  catch (err) {
397
349
  logger.error({ err, id: task.id, jid: task.jid, elapsed: elapsedLog() }, 'browser task provider call failed');
@@ -401,17 +353,6 @@ async function runBrowserTask(task) {
401
353
  });
402
354
  return;
403
355
  }
404
- // Persist the session id. On first call the provider returns the new
405
- // sessionId; on resume it may return the same or a rotated one.
406
- if (returnedSessionId) {
407
- const now = Math.floor(Date.now() / 1000);
408
- saveBrowserSession(provider.name, {
409
- sessionId: returnedSessionId,
410
- createdAt: session.createdAt || now,
411
- lastUsedAt: now,
412
- resumeCount: (session.resumeCount ?? 0) + (isResume ? 1 : 0),
413
- });
414
- }
415
356
  // Route markers the same way the general async lane does.
416
357
  const { extractFlags } = await import('../memory/digest-flag.js');
417
358
  const { clean, digest, journals, journalCreates, sendTexts } = extractFlags(reply);
@@ -487,7 +428,6 @@ async function runBrowserTask(task) {
487
428
  id: task.id,
488
429
  jid: task.jid,
489
430
  elapsed: elapsedLog(),
490
- isResume,
491
431
  appended: appendedCount,
492
432
  createdJournals: journalCreates.length,
493
433
  digestFired: !!digest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.9.11",
3
+ "version": "0.9.12",
4
4
  "description": "WhatsApp AI bot powered by Claude with long-term memory, browser control, and role-based access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",