@c4t4/heyamigo 0.9.9 → 0.9.11
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/boot.js +4 -1
- package/dist/db/schema.js +31 -0
- package/dist/gateway/incoming.js +1 -0
- package/dist/memory/digest-flag.js +17 -0
- package/dist/queue/async-tasks.js +61 -61
- package/dist/queue/memory-worker.js +169 -0
- package/dist/queue/memory-writes.js +136 -0
- package/dist/queue/orchestrator.js +5 -0
- package/dist/queue/worker.js +56 -34
- package/dist/wa/whitelist.js +26 -0
- package/migrations/0005_phase5_memory_writes.sql +17 -0
- package/migrations/meta/0005_snapshot.json +777 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
package/dist/queue/worker.js
CHANGED
|
@@ -3,10 +3,10 @@ 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';
|
|
7
|
-
import {
|
|
8
|
-
import { scheduleDigest } from '../memory/scheduler.js';
|
|
6
|
+
import { extractFlags, filterFlagsByRole } from '../memory/digest-flag.js';
|
|
7
|
+
import { isValidSlug } from '../memory/journals.js';
|
|
9
8
|
import { enqueueAsyncTask, enqueueBrowserTask } from './async-tasks.js';
|
|
9
|
+
import { enqueueMemoryWrite } from './memory-writes.js';
|
|
10
10
|
import { enqueueOutbound } from './outbound.js';
|
|
11
11
|
function isStaleSessionError(err) {
|
|
12
12
|
return (err instanceof Error &&
|
|
@@ -40,48 +40,70 @@ 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
|
+
}
|
|
63
|
+
// All memory mutations go through the memory_writes queue so the
|
|
64
|
+
// single memory worker serializes file writes — safe under parallel
|
|
65
|
+
// chat workers. Idempotency keys derived from job + index so a
|
|
66
|
+
// retry doesn't duplicate.
|
|
67
|
+
const memBase = `chat-${job.jid}-${Date.now()}`;
|
|
44
68
|
if (digest) {
|
|
45
69
|
logger.info({ jid: job.jid, number: job.senderNumber, reason: digest }, 'DIGEST flag raised, scheduling');
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
number: job.senderNumber,
|
|
49
|
-
|
|
70
|
+
enqueueMemoryWrite({
|
|
71
|
+
op: 'trigger_digest',
|
|
72
|
+
payload: { jid: job.jid, number: job.senderNumber, reason: digest },
|
|
73
|
+
idempotencyKey: `${memBase}-digest`,
|
|
50
74
|
});
|
|
51
75
|
}
|
|
52
76
|
// Creates run BEFORE entry appends so that a reply creating a new journal
|
|
53
|
-
// AND flagging its first entry in the same turn works correctly.
|
|
54
|
-
|
|
77
|
+
// AND flagging its first entry in the same turn works correctly. The
|
|
78
|
+
// memory worker enforces this ordering because it drains serially in
|
|
79
|
+
// insert order.
|
|
80
|
+
for (let i = 0; i < journalCreates.length; i++) {
|
|
81
|
+
const op = journalCreates[i];
|
|
55
82
|
if (!isValidSlug(op.slug)) {
|
|
56
83
|
logger.warn({ op, jid: job.jid }, 'JOURNAL-NEW: invalid slug, dropped');
|
|
57
84
|
continue;
|
|
58
85
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
createJournal({
|
|
65
|
-
slug: op.slug,
|
|
66
|
-
name: titleCase(op.slug),
|
|
67
|
-
purpose: op.purpose,
|
|
68
|
-
});
|
|
69
|
-
logger.info({ slug: op.slug, jid: job.jid }, 'journal created via bot marker');
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
logger.error({ err, op, jid: job.jid }, 'JOURNAL-NEW failed');
|
|
73
|
-
}
|
|
86
|
+
enqueueMemoryWrite({
|
|
87
|
+
op: 'create_journal',
|
|
88
|
+
payload: { slug: op.slug, name: titleCase(op.slug), purpose: op.purpose },
|
|
89
|
+
idempotencyKey: `${memBase}-create-${i}`,
|
|
90
|
+
});
|
|
74
91
|
}
|
|
75
|
-
for (
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
for (let i = 0; i < journals.length; i++) {
|
|
93
|
+
const j = journals[i];
|
|
94
|
+
enqueueMemoryWrite({
|
|
95
|
+
op: 'append_journal',
|
|
96
|
+
payload: {
|
|
97
|
+
slug: j.slug,
|
|
98
|
+
entry: {
|
|
99
|
+
source: 'reactive',
|
|
100
|
+
jid: job.jid,
|
|
101
|
+
senderNumber: job.senderNumber,
|
|
102
|
+
note: j.note,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
idempotencyKey: `${memBase}-append-${i}`,
|
|
81
106
|
});
|
|
82
|
-
if (!ok) {
|
|
83
|
-
logger.warn({ slug: j.slug, jid: job.jid }, 'JOURNAL flag pointed at unknown slug, dropped');
|
|
84
|
-
}
|
|
85
107
|
}
|
|
86
108
|
// Async tasks: Claude delegated to background workers. Chat reply above
|
|
87
109
|
// is the user-facing ack. Two lanes:
|
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',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CREATE TABLE `memory_writes` (
|
|
2
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
3
|
+
`op` text NOT NULL,
|
|
4
|
+
`payload` text NOT NULL,
|
|
5
|
+
`idempotency_key` text,
|
|
6
|
+
`status` text NOT NULL,
|
|
7
|
+
`attempts` integer DEFAULT 0 NOT NULL,
|
|
8
|
+
`next_attempt_at` integer,
|
|
9
|
+
`last_error` text,
|
|
10
|
+
`claimed_by` text,
|
|
11
|
+
`claimed_at` integer,
|
|
12
|
+
`created_at` integer NOT NULL,
|
|
13
|
+
`updated_at` integer NOT NULL
|
|
14
|
+
);
|
|
15
|
+
--> statement-breakpoint
|
|
16
|
+
CREATE INDEX `memwr_by_status_next` ON `memory_writes` (`status`,`next_attempt_at`);--> statement-breakpoint
|
|
17
|
+
CREATE UNIQUE INDEX `memwr_idemp_uq` ON `memory_writes` (`idempotency_key`) WHERE "memory_writes"."idempotency_key" IS NOT NULL;
|