@c4t4/heyamigo 0.9.1 → 0.9.2
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/memory/digest-flag.js +28 -0
- package/dist/queue/async-tasks.js +27 -2
- package/dist/queue/worker.js +15 -1
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ const KINDS = [
|
|
|
14
14
|
'JOURNAL-NEW',
|
|
15
15
|
'ASYNC',
|
|
16
16
|
'ASYNC-BROWSER',
|
|
17
|
+
'SEND-TEXT',
|
|
17
18
|
];
|
|
18
19
|
// Walk backwards from the end of the string, tracking bracket depth, to find
|
|
19
20
|
// the `[` that matches the final `]`. Returns the tag kind, its payload, and
|
|
@@ -74,6 +75,7 @@ export function extractFlags(reply) {
|
|
|
74
75
|
const journalCreates = [];
|
|
75
76
|
const asyncTasks = [];
|
|
76
77
|
const asyncBrowserTasks = [];
|
|
78
|
+
const sendTexts = [];
|
|
77
79
|
while (true) {
|
|
78
80
|
const peeled = peelTrailingTag(current);
|
|
79
81
|
if (!peeled)
|
|
@@ -105,6 +107,11 @@ export function extractFlags(reply) {
|
|
|
105
107
|
asyncBrowserTasks.unshift({ description: payload });
|
|
106
108
|
}
|
|
107
109
|
}
|
|
110
|
+
else if (kind === 'SEND-TEXT') {
|
|
111
|
+
const parsed = parseSendTextPayload(payload);
|
|
112
|
+
if (parsed)
|
|
113
|
+
sendTexts.unshift(parsed);
|
|
114
|
+
}
|
|
108
115
|
}
|
|
109
116
|
return {
|
|
110
117
|
clean: current,
|
|
@@ -113,6 +120,7 @@ export function extractFlags(reply) {
|
|
|
113
120
|
journalCreates,
|
|
114
121
|
asyncTasks,
|
|
115
122
|
asyncBrowserTasks,
|
|
123
|
+
sendTexts,
|
|
116
124
|
};
|
|
117
125
|
}
|
|
118
126
|
// Legacy helper kept so existing callers still compile.
|
|
@@ -121,6 +129,26 @@ export function extractDigestFlag(reply) {
|
|
|
121
129
|
return { clean: r.clean, flag: r.digest };
|
|
122
130
|
}
|
|
123
131
|
const JOURNAL_SEP_RE = /\s*(?:[—\-–]|:)\s*/;
|
|
132
|
+
// Parse `address=<addr> body="..."` style key=value payload.
|
|
133
|
+
// Body is delimited by double quotes; everything else by whitespace.
|
|
134
|
+
// Returns null if address or body is missing.
|
|
135
|
+
function parseSendTextPayload(payload) {
|
|
136
|
+
// Grab body="..." first (longest match so quoted body can contain spaces)
|
|
137
|
+
const bodyMatch = payload.match(/\bbody\s*=\s*"((?:[^"\\]|\\.)*)"/);
|
|
138
|
+
if (!bodyMatch)
|
|
139
|
+
return null;
|
|
140
|
+
const body = bodyMatch[1]
|
|
141
|
+
.replace(/\\"/g, '"')
|
|
142
|
+
.replace(/\\\\/g, '\\');
|
|
143
|
+
if (!body.trim())
|
|
144
|
+
return null;
|
|
145
|
+
// Strip the body=... portion so address parsing doesn't trip on it
|
|
146
|
+
const withoutBody = payload.replace(bodyMatch[0], '').trim();
|
|
147
|
+
const addrMatch = withoutBody.match(/\baddress\s*=\s*([^\s]+)/);
|
|
148
|
+
if (!addrMatch)
|
|
149
|
+
return null;
|
|
150
|
+
return { address: addrMatch[1], body };
|
|
151
|
+
}
|
|
124
152
|
function parseJournalPayload(payload) {
|
|
125
153
|
// Split on first em-dash, en-dash, hyphen, or colon between slug and note.
|
|
126
154
|
const match = payload.match(/^([a-zA-Z0-9][a-zA-Z0-9-]*)(.*)$/);
|
|
@@ -124,7 +124,20 @@ async function executeAsyncTask(task) {
|
|
|
124
124
|
// as markers; clean pre-marker text is only sent to chat when short (a
|
|
125
125
|
// failure explanation or tight ack) or when no markers fired at all.
|
|
126
126
|
const { extractFlags } = await import('../memory/digest-flag.js');
|
|
127
|
-
const { clean, digest, journals, journalCreates } = extractFlags(output);
|
|
127
|
+
const { clean, digest, journals, journalCreates, sendTexts } = extractFlags(output);
|
|
128
|
+
// SEND-TEXT: async task wants to text a different chat too.
|
|
129
|
+
if (sendTexts.length > 0) {
|
|
130
|
+
const { enqueueOutbound } = await import('./outbound.js');
|
|
131
|
+
for (let i = 0; i < sendTexts.length; i++) {
|
|
132
|
+
const t = sendTexts[i];
|
|
133
|
+
enqueueOutbound({
|
|
134
|
+
address: t.address,
|
|
135
|
+
kind: 'text',
|
|
136
|
+
text: t.body,
|
|
137
|
+
idempotencyKey: `async-sendtext-${task.id}-${i}`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
128
141
|
// Journal creates run first so an entry flagged in the same output against
|
|
129
142
|
// a new slug lands correctly.
|
|
130
143
|
const { appendEntry, createJournal, getJournal, isValidSlug } = await import('../memory/journals.js');
|
|
@@ -402,7 +415,19 @@ async function runBrowserTask(task) {
|
|
|
402
415
|
}
|
|
403
416
|
// Route markers the same way the general async lane does.
|
|
404
417
|
const { extractFlags } = await import('../memory/digest-flag.js');
|
|
405
|
-
const { clean, digest, journals, journalCreates } = extractFlags(reply);
|
|
418
|
+
const { clean, digest, journals, journalCreates, sendTexts } = extractFlags(reply);
|
|
419
|
+
if (sendTexts.length > 0) {
|
|
420
|
+
const { enqueueOutbound } = await import('./outbound.js');
|
|
421
|
+
for (let i = 0; i < sendTexts.length; i++) {
|
|
422
|
+
const t = sendTexts[i];
|
|
423
|
+
enqueueOutbound({
|
|
424
|
+
address: t.address,
|
|
425
|
+
kind: 'text',
|
|
426
|
+
text: t.body,
|
|
427
|
+
idempotencyKey: `browser-sendtext-${task.id}-${i}`,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
406
431
|
const { appendEntry, createJournal, getJournal, isValidSlug } = await import('../memory/journals.js');
|
|
407
432
|
for (const op of journalCreates) {
|
|
408
433
|
if (!isValidSlug(op.slug))
|
package/dist/queue/worker.js
CHANGED
|
@@ -7,6 +7,7 @@ import { extractFlags } from '../memory/digest-flag.js';
|
|
|
7
7
|
import { appendEntry, createJournal, getJournal, isValidSlug, } from '../memory/journals.js';
|
|
8
8
|
import { scheduleDigest } from '../memory/scheduler.js';
|
|
9
9
|
import { enqueueAsyncTask, enqueueBrowserTask } from './async-tasks.js';
|
|
10
|
+
import { enqueueOutbound } from './outbound.js';
|
|
10
11
|
function isStaleSessionError(err) {
|
|
11
12
|
return (err instanceof Error &&
|
|
12
13
|
err.message.includes('No conversation found'));
|
|
@@ -39,7 +40,7 @@ async function callClaude(job) {
|
|
|
39
40
|
if (job.senderNumber) {
|
|
40
41
|
addDailyTokens(job.senderNumber, usage.inputTokens + usage.outputTokens);
|
|
41
42
|
}
|
|
42
|
-
const { clean, digest, journals, journalCreates, asyncTasks, asyncBrowserTasks, } = extractFlags(reply);
|
|
43
|
+
const { clean, digest, journals, journalCreates, asyncTasks, asyncBrowserTasks, sendTexts, } = extractFlags(reply);
|
|
43
44
|
if (digest) {
|
|
44
45
|
logger.info({ jid: job.jid, number: job.senderNumber, reason: digest }, 'DIGEST flag raised, scheduling');
|
|
45
46
|
scheduleDigest({
|
|
@@ -105,6 +106,19 @@ async function callClaude(job) {
|
|
|
105
106
|
allowedTools: job.allowedTools ?? 'all',
|
|
106
107
|
});
|
|
107
108
|
}
|
|
109
|
+
// SEND-TEXT: cross-chat text send. Agent specified the destination
|
|
110
|
+
// address explicitly. Just drops a row in outbound; sender worker
|
|
111
|
+
// dispatches by channel.
|
|
112
|
+
for (let i = 0; i < sendTexts.length; i++) {
|
|
113
|
+
const t = sendTexts[i];
|
|
114
|
+
enqueueOutbound({
|
|
115
|
+
address: t.address,
|
|
116
|
+
kind: 'text',
|
|
117
|
+
text: t.body,
|
|
118
|
+
idempotencyKey: `sendtext-${job.jid}-${Date.now()}-${i}`,
|
|
119
|
+
});
|
|
120
|
+
logger.info({ from: job.jid, to: t.address, chars: t.body.length }, 'SEND-TEXT enqueued');
|
|
121
|
+
}
|
|
108
122
|
return {
|
|
109
123
|
reply: clean,
|
|
110
124
|
stats: {
|