@c4t4/heyamigo 0.8.8 → 0.8.10

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.
@@ -24,6 +24,10 @@
24
24
  "reload": ["reload"]
25
25
  },
26
26
 
27
+ "ai": {
28
+ "provider": "claude"
29
+ },
30
+
27
31
  "claude": {
28
32
  "model": "claude-opus-4-7",
29
33
  "personalityFile": "./config/personalities/sharp.md",
package/dist/ai/codex.js CHANGED
@@ -6,8 +6,8 @@
6
6
  // What's wired:
7
7
  // - exec mode with --json (NDJSON event stream on stdout)
8
8
  // - --add-dir for extra writable roots
9
- // - --sandbox-mode for tier (read-only / workspace-write / danger-full-access)
10
- // - --resume <id> for session continuation
9
+ // - --sandbox for tier (read-only / workspace-write / danger-full-access)
10
+ // - `resume <id>` subcommand for session continuation (not a flag)
11
11
  // - prompt passed on stdin (matches the spawn plumbing that already
12
12
  // pipes input to child.stdin)
13
13
  //
@@ -41,8 +41,8 @@ function systemPrompt() {
41
41
  function reloadSystemPrompt() {
42
42
  cachedSystemPrompt = null;
43
43
  }
44
- // Codex sandbox vocabulary. The CLI flag is --sandbox-mode (or --sandbox in
45
- // some builds); values are: read-only, workspace-write, danger-full-access.
44
+ // Codex sandbox vocabulary. CLI flag is --sandbox; values are: read-only,
45
+ // workspace-write, danger-full-access.
46
46
  function sandboxFor(mode) {
47
47
  switch (mode) {
48
48
  case 'read-only':
@@ -58,11 +58,12 @@ function laneTimeoutMs(lane) {
58
58
  }
59
59
  function buildExecArgs(params) {
60
60
  const args = ['exec', '--json'];
61
- args.push('--sandbox-mode', sandboxFor(params.mode));
61
+ args.push('--sandbox', sandboxFor(params.mode));
62
62
  if (params.sessionId) {
63
- // Resume keeps the prior conversation; system prompt and add-dirs
64
- // were baked in on the original turn.
65
- args.push('--resume', params.sessionId);
63
+ // Resume is a subcommand of exec, not a flag: `codex exec [opts] resume
64
+ // <SESSION_ID> [prompt]`. System prompt and add-dirs were baked in on
65
+ // the original turn so we don't re-pass them here.
66
+ args.push('resume', params.sessionId);
66
67
  }
67
68
  else {
68
69
  for (const dir of params.addDirs ?? []) {
@@ -6,6 +6,11 @@ function sessionsPath() {
6
6
  return resolve(process.cwd(), config.storage.sessionsFile);
7
7
  }
8
8
  let sessions = load();
9
+ // Migration from v0.8.x flat format: previously `sessions.json` held either
10
+ // `{ jid: "session-string" }` or `{ jid: { sessionId, usage } }`. Both meant
11
+ // "Claude session for this jid" because Claude was the only provider. We
12
+ // attribute legacy entries to `claude` so existing installs don't lose state
13
+ // when they upgrade.
9
14
  function load() {
10
15
  const path = sessionsPath();
11
16
  if (!existsSync(path))
@@ -15,10 +20,28 @@ function load() {
15
20
  const out = {};
16
21
  for (const [jid, v] of Object.entries(raw)) {
17
22
  if (typeof v === 'string') {
18
- out[jid] = { sessionId: v }; // migrate old flat string format
23
+ // legacy flat string
24
+ out[jid] = { claude: { sessionId: v } };
19
25
  }
20
- else if (v && typeof v === 'object' && 'sessionId' in v) {
21
- out[jid] = v;
26
+ else if (v && typeof v === 'object') {
27
+ const obj = v;
28
+ // Detect already-namespaced format: at least one key is a known
29
+ // ProviderName whose value looks like a Session.
30
+ const isNamespaced = ('claude' in obj &&
31
+ typeof obj.claude === 'object' &&
32
+ obj.claude !== null &&
33
+ 'sessionId' in obj.claude) ||
34
+ ('codex' in obj &&
35
+ typeof obj.codex === 'object' &&
36
+ obj.codex !== null &&
37
+ 'sessionId' in obj.codex);
38
+ if (isNamespaced) {
39
+ out[jid] = obj;
40
+ }
41
+ else if ('sessionId' in obj) {
42
+ // legacy single-Session shape
43
+ out[jid] = { claude: obj };
44
+ }
22
45
  }
23
46
  }
24
47
  return out;
@@ -33,31 +56,47 @@ function save() {
33
56
  mkdirSync(dirname(path), { recursive: true });
34
57
  writeFileSync(path, JSON.stringify(sessions, null, 2) + '\n', 'utf-8');
35
58
  }
36
- export function getSession(jid) {
37
- return sessions[jid]?.sessionId;
59
+ export function getSession(jid, provider) {
60
+ return sessions[jid]?.[provider]?.sessionId;
38
61
  }
39
- export function getSessionInfo(jid) {
40
- return sessions[jid];
62
+ export function getSessionInfo(jid, provider) {
63
+ return sessions[jid]?.[provider];
41
64
  }
42
- export function setSession(jid, sessionId) {
43
- const existing = sessions[jid];
44
- sessions[jid] = { sessionId, usage: existing?.usage };
65
+ export function setSession(jid, provider, sessionId) {
66
+ const bucket = sessions[jid] ?? {};
67
+ const existing = bucket[provider];
68
+ bucket[provider] = { sessionId, usage: existing?.usage };
69
+ sessions[jid] = bucket;
45
70
  save();
46
71
  }
47
- export function setUsage(jid, usage) {
48
- const existing = sessions[jid];
72
+ export function setUsage(jid, provider, usage) {
73
+ const existing = sessions[jid]?.[provider];
49
74
  if (!existing)
50
75
  return;
51
- sessions[jid] = { ...existing, usage };
76
+ const bucket = sessions[jid];
77
+ bucket[provider] = { ...existing, usage };
52
78
  save();
53
79
  }
54
- export function clearSession(jid) {
55
- if (!(jid in sessions))
80
+ // Clears the session for one provider on this jid. Returns true if a session
81
+ // was actually removed. Other providers' sessions on the same jid are left
82
+ // alone — they're independent.
83
+ export function clearSession(jid, provider) {
84
+ const bucket = sessions[jid];
85
+ if (!bucket || !bucket[provider])
56
86
  return false;
57
- delete sessions[jid];
87
+ delete bucket[provider];
88
+ if (Object.keys(bucket).length === 0) {
89
+ delete sessions[jid];
90
+ }
91
+ else {
92
+ sessions[jid] = bucket;
93
+ }
58
94
  save();
59
95
  return true;
60
96
  }
97
+ // Returns every (jid, provider) pair currently stored. Used by the sweeper to
98
+ // discover jids with activity — it doesn't care which provider produced the
99
+ // session id.
61
100
  export function listSessions() {
62
101
  return sessions;
63
102
  }
@@ -1,5 +1,5 @@
1
1
  import { clearSession, getSessionInfo } from '../ai/sessions.js';
2
- import { reloadAllSystemPrompts } from '../ai/providers.js';
2
+ import { getProvider, reloadAllSystemPrompts } from '../ai/providers.js';
3
3
  import { config } from '../config.js';
4
4
  import { runDigestNow } from '../memory/scheduler.js';
5
5
  import { sendText } from '../wa/sender.js';
@@ -16,15 +16,16 @@ export async function tryCommand(ctx) {
16
16
  if (!cmd)
17
17
  return false;
18
18
  if (config.commands.reset.includes(cmd)) {
19
- const existed = clearSession(ctx.jid);
19
+ const provider = getProvider();
20
+ const existed = clearSession(ctx.jid, provider.name);
20
21
  const reply = existed
21
- ? 'Session reset. Next message will bootstrap a fresh Claude session.'
22
+ ? `Session reset. Next message will bootstrap a fresh ${provider.name} session.`
22
23
  : 'No session to reset.';
23
24
  await sendText(ctx.sock, ctx.jid, reply, ctx.quoted);
24
25
  return true;
25
26
  }
26
27
  if (config.commands.status.includes(cmd)) {
27
- const info = getSessionInfo(ctx.jid);
28
+ const info = getSessionInfo(ctx.jid, getProvider().name);
28
29
  if (!info) {
29
30
  await sendText(ctx.sock, ctx.jid, 'No session yet. Next message will bootstrap one.', ctx.quoted);
30
31
  return true;
@@ -42,7 +43,7 @@ export async function tryCommand(ctx) {
42
43
  }
43
44
  if (config.commands.reload.includes(cmd)) {
44
45
  reloadAllSystemPrompts();
45
- const existed = clearSession(ctx.jid);
46
+ const existed = clearSession(ctx.jid, getProvider().name);
46
47
  const reply = existed
47
48
  ? 'Personality reloaded and session reset.'
48
49
  : 'Personality reloaded.';
@@ -1,5 +1,6 @@
1
1
  import { unlink } from 'fs/promises';
2
2
  import { getContentType, isJidGroup, jidDecode, jidNormalizedUser, } from 'baileys';
3
+ import { getProvider } from '../ai/providers.js';
3
4
  import { getSession } from '../ai/sessions.js';
4
5
  import { config } from '../config.js';
5
6
  import { logger } from '../logger.js';
@@ -162,7 +163,7 @@ async function processMessages(messages, sock, ownerJid, isHistorySync = false)
162
163
  }
163
164
  }
164
165
  const { role } = getRoleForContext(stored.senderNumber, isGroup);
165
- const existingSession = getSession(stored.jid);
166
+ const existingSession = getSession(stored.jid, getProvider().name);
166
167
  let userContent = stored.text;
167
168
  if (media) {
168
169
  const tag = mediaPromptTag(media, stored.text);
@@ -231,16 +231,31 @@ function truncate(s, n) {
231
231
  // memory of prior tasks across runs.
232
232
  // - Task description is added as a new user message to the persistent
233
233
  // session. The worker sees the accumulated history automatically.
234
- function browserSessionFilePath() {
234
+ // Per-provider browser session storage. Each CLI's session ids are opaque
235
+ // to the other, so swapping providers must not feed one's session id to
236
+ // the other. Filename includes the provider name to keep them separate;
237
+ // the legacy provider-less filename is auto-migrated to claude on read.
238
+ function browserSessionFilePath(provider) {
239
+ return resolve(process.cwd(), config.memory.dir, `browser-session-${provider}.json`);
240
+ }
241
+ function legacyBrowserSessionFilePath() {
235
242
  return resolve(process.cwd(), config.memory.dir, 'browser-session.json');
236
243
  }
237
- function loadBrowserSession() {
238
- const path = browserSessionFilePath();
244
+ function loadBrowserSession(provider) {
245
+ const path = browserSessionFilePath(provider);
246
+ let source = path;
239
247
  if (!existsSync(path)) {
240
- return { sessionId: null, createdAt: 0, lastUsedAt: 0, resumeCount: 0 };
248
+ const legacy = legacyBrowserSessionFilePath();
249
+ if (provider === 'claude' && existsSync(legacy)) {
250
+ // One-time migration: legacy file was implicitly claude.
251
+ source = legacy;
252
+ }
253
+ else {
254
+ return { sessionId: null, createdAt: 0, lastUsedAt: 0, resumeCount: 0 };
255
+ }
241
256
  }
242
257
  try {
243
- const parsed = JSON.parse(readFileSync(path, 'utf-8'));
258
+ const parsed = JSON.parse(readFileSync(source, 'utf-8'));
244
259
  return {
245
260
  sessionId: parsed.sessionId ?? null,
246
261
  createdAt: parsed.createdAt ?? 0,
@@ -252,21 +267,23 @@ function loadBrowserSession() {
252
267
  return { sessionId: null, createdAt: 0, lastUsedAt: 0, resumeCount: 0 };
253
268
  }
254
269
  }
255
- function saveBrowserSession(state) {
256
- const path = browserSessionFilePath();
270
+ function saveBrowserSession(provider, state) {
271
+ const path = browserSessionFilePath(provider);
257
272
  mkdirSync(dirname(path), { recursive: true });
258
273
  writeFileSync(path, JSON.stringify(state, null, 2) + '\n', 'utf-8');
259
274
  }
260
- // Reset the browser session. Callable from outside if the session gets
261
- // corrupted or we want a fresh start. Not wired into any command yet.
275
+ // Reset the browser session for the active provider. Callable from outside
276
+ // if the session gets corrupted or we want a fresh start. Not wired into
277
+ // any command yet.
262
278
  export function resetBrowserSession() {
263
- saveBrowserSession({
279
+ const provider = getProvider().name;
280
+ saveBrowserSession(provider, {
264
281
  sessionId: null,
265
282
  createdAt: 0,
266
283
  lastUsedAt: 0,
267
284
  resumeCount: 0,
268
285
  });
269
- logger.info('browser session reset');
286
+ logger.info({ provider }, 'browser session reset');
270
287
  }
271
288
  const browserQueue = fastq.promise(async (task) => {
272
289
  inProgress.set(task.id, task);
@@ -343,14 +360,15 @@ function browserAddDirs() {
343
360
  ];
344
361
  }
345
362
  async function runBrowserTask(task) {
346
- const session = loadBrowserSession();
363
+ const provider = getProvider();
364
+ const session = loadBrowserSession(provider.name);
347
365
  const isResume = !!session.sessionId;
348
366
  const prompt = buildBrowserPrompt(task, isResume);
349
367
  const elapsedLog = () => `${Math.round((Date.now() - task.startedAt * 1000) / 1000)}s`;
350
368
  let reply;
351
369
  let returnedSessionId;
352
370
  try {
353
- const result = await getProvider().runTask({
371
+ const result = await provider.runTask({
354
372
  input: prompt,
355
373
  caller: 'browser-task',
356
374
  mode: 'auto',
@@ -375,7 +393,7 @@ async function runBrowserTask(task) {
375
393
  // sessionId; on resume it may return the same or a rotated one.
376
394
  if (returnedSessionId) {
377
395
  const now = Math.floor(Date.now() / 1000);
378
- saveBrowserSession({
396
+ saveBrowserSession(provider.name, {
379
397
  sessionId: returnedSessionId,
380
398
  createdAt: session.createdAt || now,
381
399
  lastUsedAt: now,
@@ -14,20 +14,21 @@ function isStaleSessionError(err) {
14
14
  async function callClaude(job) {
15
15
  const startedAt = Date.now();
16
16
  const wasFresh = !job.sessionId;
17
- const { reply, sessionId, usage } = await getProvider().ask({
17
+ const provider = getProvider();
18
+ const { reply, sessionId, usage } = await provider.ask({
18
19
  input: job.input,
19
20
  sessionId: job.sessionId,
20
21
  allowedTools: job.allowedTools,
21
22
  });
22
23
  const durationMs = Date.now() - startedAt;
23
24
  if (!job.sessionId) {
24
- setSession(job.jid, sessionId);
25
+ setSession(job.jid, provider.name, sessionId);
25
26
  }
26
27
  const totalContextTokens = usage.inputTokens +
27
28
  usage.cacheReadTokens +
28
29
  usage.cacheCreationTokens +
29
30
  usage.outputTokens;
30
- setUsage(job.jid, {
31
+ setUsage(job.jid, provider.name, {
31
32
  ...usage,
32
33
  totalContextTokens,
33
34
  updatedAt: Math.floor(Date.now() / 1000),
@@ -133,7 +134,7 @@ export async function processJob(job) {
133
134
  catch (err) {
134
135
  if (job.sessionId && isStaleSessionError(err)) {
135
136
  logger.warn({ jid: job.jid, staleId: job.sessionId }, 'stale session detected, clearing and retrying with fresh bootstrap');
136
- clearSession(job.jid);
137
+ clearSession(job.jid, getProvider().name);
137
138
  return callClaude({ ...job, sessionId: undefined, allowedTools: job.allowedTools });
138
139
  }
139
140
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
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",