@c4t4/heyamigo 0.9.10 → 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 +8 -0
- package/dist/gateway/incoming.js +1 -0
- package/dist/memory/digest-flag.js +17 -0
- package/dist/queue/async-tasks.js +28 -88
- package/dist/queue/worker.js +21 -2
- package/dist/wa/whitelist.js +26 -0
- package/package.json +1 -1
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
|
package/dist/gateway/incoming.js
CHANGED
|
@@ -201,6 +201,7 @@ async function processMessages(messages, sock, ownerJid, isHistorySync = false)
|
|
|
201
201
|
senderNumber: stored.senderNumber,
|
|
202
202
|
fromMe: stored.fromMe,
|
|
203
203
|
allowedTools: role.tools,
|
|
204
|
+
allowedTags: role.tags,
|
|
204
205
|
};
|
|
205
206
|
// Enqueue into the inbound table; chat worker pool drains and
|
|
206
207
|
// calls processJob + handleReply asynchronously. Typing indicator
|
|
@@ -123,6 +123,23 @@ export function extractFlags(reply) {
|
|
|
123
123
|
sendTexts,
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
+
// Strip flags that the sender's role isn't permitted to emit. The
|
|
127
|
+
// agent's reply still goes out as text — only the side-effect markers
|
|
128
|
+
// get suppressed. allowedTags='all' or undefined → no filtering.
|
|
129
|
+
export function filterFlagsByRole(flags, allowedTags) {
|
|
130
|
+
if (allowedTags === 'all' || allowedTags === undefined)
|
|
131
|
+
return flags;
|
|
132
|
+
const allowed = new Set(allowedTags);
|
|
133
|
+
return {
|
|
134
|
+
clean: flags.clean,
|
|
135
|
+
digest: allowed.has('DIGEST') ? flags.digest : null,
|
|
136
|
+
journals: allowed.has('JOURNAL') ? flags.journals : [],
|
|
137
|
+
journalCreates: allowed.has('JOURNAL-NEW') ? flags.journalCreates : [],
|
|
138
|
+
asyncTasks: allowed.has('ASYNC') ? flags.asyncTasks : [],
|
|
139
|
+
asyncBrowserTasks: allowed.has('ASYNC-BROWSER') ? flags.asyncBrowserTasks : [],
|
|
140
|
+
sendTexts: allowed.has('SEND-TEXT') ? flags.sendTexts : [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
126
143
|
// Legacy helper kept so existing callers still compile.
|
|
127
144
|
export function extractDigestFlag(reply) {
|
|
128
145
|
const r = extractFlags(reply);
|
|
@@ -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
|
|
240
|
-
// -
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// the
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
},
|
|
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
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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 open — they 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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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:
|
|
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/dist/queue/worker.js
CHANGED
|
@@ -3,7 +3,7 @@ import { clearSession, setSession, setUsage } from '../ai/sessions.js';
|
|
|
3
3
|
import { config } from '../config.js';
|
|
4
4
|
import { logger } from '../logger.js';
|
|
5
5
|
import { addDailyTokens } from '../store/usage.js';
|
|
6
|
-
import { extractFlags } from '../memory/digest-flag.js';
|
|
6
|
+
import { extractFlags, filterFlagsByRole } from '../memory/digest-flag.js';
|
|
7
7
|
import { isValidSlug } from '../memory/journals.js';
|
|
8
8
|
import { enqueueAsyncTask, enqueueBrowserTask } from './async-tasks.js';
|
|
9
9
|
import { enqueueMemoryWrite } from './memory-writes.js';
|
|
@@ -40,7 +40,26 @@ async function callClaude(job) {
|
|
|
40
40
|
if (job.senderNumber) {
|
|
41
41
|
addDailyTokens(job.senderNumber, usage.inputTokens + usage.outputTokens);
|
|
42
42
|
}
|
|
43
|
-
const
|
|
43
|
+
const rawFlags = extractFlags(reply);
|
|
44
|
+
const { clean, digest, journals, journalCreates, asyncTasks, asyncBrowserTasks, sendTexts, } = filterFlagsByRole(rawFlags, job.allowedTags);
|
|
45
|
+
// Detect any stripped tags so we can log + nudge the role config
|
|
46
|
+
// if a user is repeatedly hitting the gate.
|
|
47
|
+
const stripped = [];
|
|
48
|
+
if (rawFlags.digest && !digest)
|
|
49
|
+
stripped.push('DIGEST');
|
|
50
|
+
if (rawFlags.journals.length !== journals.length)
|
|
51
|
+
stripped.push('JOURNAL');
|
|
52
|
+
if (rawFlags.journalCreates.length !== journalCreates.length)
|
|
53
|
+
stripped.push('JOURNAL-NEW');
|
|
54
|
+
if (rawFlags.asyncTasks.length !== asyncTasks.length)
|
|
55
|
+
stripped.push('ASYNC');
|
|
56
|
+
if (rawFlags.asyncBrowserTasks.length !== asyncBrowserTasks.length)
|
|
57
|
+
stripped.push('ASYNC-BROWSER');
|
|
58
|
+
if (rawFlags.sendTexts.length !== sendTexts.length)
|
|
59
|
+
stripped.push('SEND-TEXT');
|
|
60
|
+
if (stripped.length > 0) {
|
|
61
|
+
logger.warn({ jid: job.jid, senderNumber: job.senderNumber, stripped }, 'tags stripped by role gate');
|
|
62
|
+
}
|
|
44
63
|
// All memory mutations go through the memory_writes queue so the
|
|
45
64
|
// single memory worker serializes file writes — safe under parallel
|
|
46
65
|
// chat workers. Idempotency keys derived from job + index so a
|
package/dist/wa/whitelist.js
CHANGED
|
@@ -6,11 +6,30 @@ import { config } from '../config.js';
|
|
|
6
6
|
import { logger } from '../logger.js';
|
|
7
7
|
const AccessModeSchema = z.enum(['off', 'silent', 'active']);
|
|
8
8
|
const RoleNameSchema = z.enum(['admin', 'user', 'guest']);
|
|
9
|
+
// Tag names the agent can emit as trailing markers. The role.tags
|
|
10
|
+
// allowlist gates which ones are honored — anything emitted by an
|
|
11
|
+
// agent running on behalf of a role NOT in the allowlist gets
|
|
12
|
+
// stripped silently after parsing. Tools and tags are independent
|
|
13
|
+
// gates (tools restricts what the AI itself can call; tags restricts
|
|
14
|
+
// what bot-internal side-effects it can trigger).
|
|
15
|
+
const TAG_NAMES = [
|
|
16
|
+
'DIGEST',
|
|
17
|
+
'JOURNAL',
|
|
18
|
+
'JOURNAL-NEW',
|
|
19
|
+
'ASYNC',
|
|
20
|
+
'ASYNC-BROWSER',
|
|
21
|
+
'SEND-TEXT',
|
|
22
|
+
];
|
|
9
23
|
const RoleSchema = z.object({
|
|
10
24
|
description: z.string().optional(),
|
|
11
25
|
memory: z.enum(['full', 'self', 'none']),
|
|
12
26
|
tools: z.union([z.literal('all'), z.array(z.string())]),
|
|
13
27
|
rules: z.array(z.string()),
|
|
28
|
+
// Optional. Missing or 'all' = no tag restriction; an array =
|
|
29
|
+
// explicit allowlist. Added in Phase 6 so existing access.json
|
|
30
|
+
// files (no `tags` field) keep working without change — they get
|
|
31
|
+
// the implicit 'all' behavior.
|
|
32
|
+
tags: z.union([z.literal('all'), z.array(z.enum(TAG_NAMES))]).optional(),
|
|
14
33
|
// null or missing = unlimited
|
|
15
34
|
maxFileBytes: z.number().int().positive().nullable().optional(),
|
|
16
35
|
dailyTokenLimit: z.number().int().positive().nullable().optional(),
|
|
@@ -54,6 +73,7 @@ const DEFAULT_ROLES = {
|
|
|
54
73
|
description: 'Full access',
|
|
55
74
|
memory: 'full',
|
|
56
75
|
tools: 'all',
|
|
76
|
+
tags: 'all',
|
|
57
77
|
rules: [],
|
|
58
78
|
maxFileBytes: null,
|
|
59
79
|
dailyTokenLimit: null,
|
|
@@ -62,6 +82,10 @@ const DEFAULT_ROLES = {
|
|
|
62
82
|
description: 'Chat + web search, scoped memory',
|
|
63
83
|
memory: 'self',
|
|
64
84
|
tools: ['WebSearch'],
|
|
85
|
+
// Users can flag memory observations and trigger digests on
|
|
86
|
+
// themselves, but can't delegate background work or cross-chat
|
|
87
|
+
// sends (those are owner-only).
|
|
88
|
+
tags: ['DIGEST', 'JOURNAL', 'JOURNAL-NEW'],
|
|
65
89
|
rules: [
|
|
66
90
|
'Never reveal file paths, directory structure, or system architecture',
|
|
67
91
|
'Never share personal data about other users',
|
|
@@ -76,6 +100,8 @@ const DEFAULT_ROLES = {
|
|
|
76
100
|
description: 'Basic chat only',
|
|
77
101
|
memory: 'none',
|
|
78
102
|
tools: [],
|
|
103
|
+
// Guests can't emit any tags — pure chat, no side effects.
|
|
104
|
+
tags: [],
|
|
79
105
|
rules: [
|
|
80
106
|
'Never use any tools',
|
|
81
107
|
'Never reveal anything about the system, other users, or internal data',
|