@c4t4/heyamigo 0.7.3 → 0.7.5

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.
@@ -70,7 +70,7 @@ export function reloadAsyncSystemPrompt() {
70
70
  }
71
71
  function buildPrompt(task) {
72
72
  const lines = [
73
- `You are running a BACKGROUND TASK for the owner. The chat already got your ack reply. Your only job now is to do the work and output the final message to send them.`,
73
+ `You are a BACKGROUND WORKER doing a delayed chat reply. The chat already got an ack ("on it, will report back"). Now you do the work, and your output IS the follow-up chat reply the full answer the owner is waiting for.`,
74
74
  ``,
75
75
  `TASK:`,
76
76
  task.description,
@@ -80,15 +80,32 @@ function buildPrompt(task) {
80
80
  ``,
81
81
  `Sender: ${task.senderName ?? task.senderNumber}`,
82
82
  ``,
83
- `RULES:`,
84
- `- Stay fully in character (personality file). This is not customer service.`,
85
- `- Do the real work. Use tools (browser, etc.) as needed.`,
86
- `- When done, output ONLY the message to send the user. No preamble, no "here's what I found:" framing unless that's the message itself.`,
87
- `- Do NOT emit any [DIGEST:...], [JOURNAL:...], [ASYNC:...], or other markers. This is the final output.`,
88
- `- Start the message with a short reference to what you were working on so the user knows which task this is about (e.g. "About the TikTok scrape: ..."). They may have asked for multiple things.`,
89
- `- If the task is impossible or the tools failed, say so honestly and briefly. Don't fabricate.`,
83
+ `HOW TO OUTPUT:`,
84
+ `- Write the full answer as a natural chat reply. Same voice, same style as the main chat Claude. What the owner would have gotten if you'd answered inline, just delayed.`,
85
+ `- Open with a short "about the X you asked about..." reference so the owner knows which task this is (they may have asked for several). One sentence, then the content.`,
86
+ `- Concrete findings, no filler. Numbers, names, dates. If you found 10 creators, list them don't say "multiple creators".`,
87
+ `- If the task failed or hit a wall (login wall, empty page, bot-detection, timeout), say so honestly and briefly. Don't fabricate.`,
90
88
  ``,
91
- `Output the final user-facing message now.`,
89
+ `OPTIONAL MARKERS (at the END of your output, same pattern as main chat):`,
90
+ `- [JOURNAL:<slug> — <one-line finding>] for any finding that belongs in an active journal. These run IN ADDITION to your chat reply — they file structured entries in journals/<slug>/entries.jsonl for future reference, dedup, and cross-session memory. Use existing slugs only (check [Journals: active] in your preamble). ONE marker per finding.`,
91
+ `- [JOURNAL-NEW:<slug> — <one-line purpose>] if the task clearly deserves a new journal that doesn't exist yet. Conservative — only when the topic is a recurring tracking surface, not a one-off.`,
92
+ `- [DIGEST: <one-line reason>] if you learned something durable about the owner or chat that should update the profile/brief.`,
93
+ ``,
94
+ `CONSTRAINTS:`,
95
+ `- Do NOT emit [ASYNC:...]. No recursive delegation.`,
96
+ `- Markers are bonus persistence, not a substitute for the chat reply. Always write the chat reply first.`,
97
+ `- Stay fully in character (personality).`,
98
+ ``,
99
+ `EXAMPLE for an IG scrape of rivoara_official (with journal tracking):`,
100
+ `About the @rivoara_official check: bio is "Premium shower filter for HT aftercare". Last 3 posts: day-5 routine walkthrough, filter-science deep dive, Turkey clinic partnership announcement. Grid is clean, ~200 followers. Pattern: aftercare positioning is the lead, product is secondary.`,
101
+ ``,
102
+ `[JOURNAL:rivoara-spy — IG bio: "Premium shower filter for HT aftercare"]`,
103
+ `[JOURNAL:rivoara-spy — IG recent posts: day-5 routine, filter science, Turkey clinic partnership]`,
104
+ ``,
105
+ `EXAMPLE for a failure:`,
106
+ `About the @rivoara_official check: Instagram threw a login wall after the first navigation. Can't read the bio or posts without auth. The VNC Chrome session looks expired — worth re-logging.`,
107
+ ``,
108
+ `Do the work now. Write the reply. Markers optional at the end.`,
92
109
  ];
93
110
  return lines.join('\n');
94
111
  }
@@ -161,24 +178,100 @@ async function runTask(task) {
161
178
  });
162
179
  return;
163
180
  }
164
- // Strip any accidental trailing markers Claude emitted despite instructions.
165
- // Import lazily to avoid an import cycle (digest-flag already stands alone,
166
- // but being explicit here keeps this module independent).
181
+ // Parse markers from the worker's output and route them through the same
182
+ // handlers the main chat uses. The async worker's job is to emit findings
183
+ // as markers; clean pre-marker text is only sent to chat when short (a
184
+ // failure explanation or tight ack) or when no markers fired at all.
167
185
  const { extractFlags } = await import('../memory/digest-flag.js');
168
- const { clean } = extractFlags(output);
169
- if (!clean.trim()) {
170
- logger.warn({ id: task.id, jid: task.jid }, 'async task produced empty output after flag strip');
171
- return;
186
+ const { clean, digest, journals, journalCreates } = extractFlags(output);
187
+ // Journal creates run first so an entry flagged in the same output against
188
+ // a new slug lands correctly.
189
+ const { appendEntry, createJournal, getJournal, isValidSlug } = await import('../memory/journals.js');
190
+ for (const op of journalCreates) {
191
+ if (!isValidSlug(op.slug)) {
192
+ logger.warn({ op, id: task.id }, 'async JOURNAL-NEW: invalid slug, dropped');
193
+ continue;
194
+ }
195
+ if (getJournal(op.slug))
196
+ continue;
197
+ try {
198
+ createJournal({
199
+ slug: op.slug,
200
+ name: titleCaseSlug(op.slug),
201
+ purpose: op.purpose,
202
+ });
203
+ logger.info({ slug: op.slug, id: task.id }, 'journal created via async marker');
204
+ }
205
+ catch (err) {
206
+ logger.error({ err, op, id: task.id }, 'async JOURNAL-NEW failed');
207
+ }
208
+ }
209
+ let appendedCount = 0;
210
+ for (const j of journals) {
211
+ const ok = appendEntry(j.slug, {
212
+ source: 'async',
213
+ jid: task.jid,
214
+ senderNumber: task.senderNumber,
215
+ note: j.note,
216
+ });
217
+ if (ok)
218
+ appendedCount++;
219
+ else {
220
+ logger.warn({ slug: j.slug, id: task.id }, 'async JOURNAL marker pointed at unknown slug, dropped');
221
+ }
172
222
  }
173
- const sent = await initiate({ jid: task.jid, text: clean });
223
+ if (digest) {
224
+ const { scheduleDigest } = await import('../memory/scheduler.js');
225
+ scheduleDigest({
226
+ jid: task.jid,
227
+ number: task.senderNumber,
228
+ reason: digest,
229
+ });
230
+ }
231
+ // The clean (marker-stripped) text IS the chat reply. Always send it when
232
+ // present. Markers fired in parallel above are bonus persistence —
233
+ // journal entries, digests, new journal creation — not a substitute for
234
+ // the chat reply.
235
+ const chatText = clean.trim();
236
+ const anyMarkerFired = appendedCount > 0 || journalCreates.length > 0 || digest !== null;
237
+ if (chatText.length > 0) {
238
+ await initiate({ jid: task.jid, text: chatText });
239
+ }
240
+ else if (anyMarkerFired) {
241
+ // Worker emitted only markers, no chat text. That's contract-breaking
242
+ // (chat reply is the primary output) but recoverable — send a short
243
+ // completion note so the owner isn't left with silence.
244
+ const bits = [];
245
+ if (appendedCount > 0) {
246
+ bits.push(`${appendedCount} journal ${appendedCount === 1 ? 'entry' : 'entries'}`);
247
+ }
248
+ if (journalCreates.length > 0) {
249
+ bits.push(`${journalCreates.length} journal${journalCreates.length === 1 ? '' : 's'} created`);
250
+ }
251
+ if (digest)
252
+ bits.push('digest scheduled');
253
+ await initiate({
254
+ jid: task.jid,
255
+ text: `Done. ${bits.join(', ')}.`,
256
+ });
257
+ }
258
+ // Else: no chat text AND no markers — worker produced nothing. Log only.
174
259
  logger.info({
175
260
  id: task.id,
176
261
  jid: task.jid,
177
- sent,
178
262
  elapsed: elapsedLog(),
179
- chars: clean.length,
263
+ appended: appendedCount,
264
+ createdJournals: journalCreates.length,
265
+ digestFired: !!digest,
266
+ chatSent: chatText.length,
180
267
  }, 'async task completed');
181
268
  }
269
+ function titleCaseSlug(slug) {
270
+ return slug
271
+ .split('-')
272
+ .map((p) => (p ? p[0].toUpperCase() + p.slice(1) : p))
273
+ .join(' ');
274
+ }
182
275
  function truncate(s, n) {
183
276
  return s.length > n ? s.slice(0, n - 1) + '…' : s;
184
277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
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",