@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.
- package/config/config.example.json +4 -0
- package/dist/ai/codex.js +9 -8
- package/dist/ai/sessions.js +55 -16
- package/dist/gateway/commands.js +6 -5
- package/dist/gateway/incoming.js +2 -1
- package/dist/queue/async-tasks.js +32 -14
- package/dist/queue/worker.js +5 -4
- package/package.json +1 -1
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
|
|
10
|
-
// -
|
|
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.
|
|
45
|
-
//
|
|
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
|
|
61
|
+
args.push('--sandbox', sandboxFor(params.mode));
|
|
62
62
|
if (params.sessionId) {
|
|
63
|
-
// Resume
|
|
64
|
-
// were baked in on
|
|
65
|
-
|
|
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 ?? []) {
|
package/dist/ai/sessions.js
CHANGED
|
@@ -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
|
-
|
|
23
|
+
// legacy flat string
|
|
24
|
+
out[jid] = { claude: { sessionId: v } };
|
|
19
25
|
}
|
|
20
|
-
else if (v && typeof v === 'object'
|
|
21
|
-
|
|
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
|
|
44
|
-
|
|
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]
|
|
76
|
+
const bucket = sessions[jid];
|
|
77
|
+
bucket[provider] = { ...existing, usage };
|
|
52
78
|
save();
|
|
53
79
|
}
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
}
|
package/dist/gateway/commands.js
CHANGED
|
@@ -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
|
|
19
|
+
const provider = getProvider();
|
|
20
|
+
const existed = clearSession(ctx.jid, provider.name);
|
|
20
21
|
const reply = existed
|
|
21
|
-
?
|
|
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.';
|
package/dist/gateway/incoming.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
261
|
-
// corrupted or we want a fresh start. Not wired into
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
package/dist/queue/worker.js
CHANGED
|
@@ -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
|
|
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;
|