@yemi33/minions 0.1.2373 → 0.1.2374
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.
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# inbox_entries Table Schema
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
The `inbox_entries` table is the persistent store for operator and agent inbox messages within the Minions orchestration engine. It provides a time-ordered, unread-tracking message queue decoupled from the runtime work-item/dispatch lifecycle.
|
|
5
|
+
|
|
6
|
+
## Schema Specification
|
|
7
|
+
|
|
8
|
+
### Column Definitions
|
|
9
|
+
|
|
10
|
+
| Column | Type | Constraints | Purpose |
|
|
11
|
+
|--------|------|-----------|---------|
|
|
12
|
+
| `id` | TEXT | PRIMARY KEY | Unique message identifier; generated as `<recipient>-<timestamp>-<sender>-<seq>` format |
|
|
13
|
+
| `recipient_session_id` | TEXT | NOT NULL | Session ID of the inbox owner (operator or agent); routes messages to specific actor |
|
|
14
|
+
| `sender_id` | TEXT | NOT NULL | Identifier of the message originator; structured as `<agent-id>` or `'system'` or `'operator'` |
|
|
15
|
+
| `sender_name` | TEXT | NOT NULL | Human-readable sender name for UI display; persisted at write-time for offline/archived reference |
|
|
16
|
+
| `sender_type` | TEXT | NOT NULL | Enum: `'agent'`, `'system'`, `'operator'`; constrains allowed sender_id values |
|
|
17
|
+
| `interaction_id` | TEXT | NOT NULL | Group ID linking related messages (e.g., work-item dispatch feedback, PR review batch); enables threading and deduplication |
|
|
18
|
+
| `sequence` | INTEGER | NOT NULL, DEFAULT 0 | Per-interaction message counter; disambiguates bursts and enables ordering within same millisecond |
|
|
19
|
+
| `summary` | TEXT | NOT NULL | Title/subject line; used for list views and user scanning; always present even if content is truncated |
|
|
20
|
+
| `content` | TEXT | NOT NULL | Full message body; may contain Markdown, code blocks, or structured JSON sidecars (not yet parsed at table level) |
|
|
21
|
+
| `unread` | INTEGER | NOT NULL, DEFAULT 1 | Boolean flag (0/1); toggled by reader, cleared on archive/delete; enables fast unread-count queries |
|
|
22
|
+
| `sent_at` | INTEGER | NOT NULL | Unix timestamp (milliseconds) of message creation; primary sort key for time-ordered display |
|
|
23
|
+
| `read_at` | INTEGER | Nullable | Unix timestamp (milliseconds) when recipient first read the message; null until marked read |
|
|
24
|
+
| `notified_at` | INTEGER | Nullable | Unix timestamp (milliseconds) of last notification event sent to operator; null if no notification dispatched |
|
|
25
|
+
|
|
26
|
+
### Constraints & Relationships
|
|
27
|
+
|
|
28
|
+
- **PK constraint:** `id` is globally unique across all inboxes; collision risk is negligible given the composite structure (recipient + timestamp + sender + seq).
|
|
29
|
+
- **FK (soft):** `recipient_session_id` does NOT enforce a referential constraint to `sessions` table (sessions may be archived). Queries SHOULD handle missing recipients gracefully.
|
|
30
|
+
- **FK (soft):** `sender_id` is informational only; no validation that the sender exists in the `config.agents` or active sessions. Invalid senders are logged but do not block writes.
|
|
31
|
+
- **No composite index on (recipient_session_id, sent_at):** See Performance section.
|
|
32
|
+
|
|
33
|
+
## Architectural Rationale
|
|
34
|
+
|
|
35
|
+
### Design Decisions
|
|
36
|
+
|
|
37
|
+
1. **Separate inbox table vs. inline dispatch/work-item attachments.**
|
|
38
|
+
- Inboxes can outlive their source work items (e.g., a feedback message remains useful after the WI is done).
|
|
39
|
+
- Decoupling allows independent inbox lifecycle (archive, export, cleanup) without cascading work-item deletes.
|
|
40
|
+
- Supports rich async consolidation (team notes, KB articles) that don't belong to any single WI.
|
|
41
|
+
|
|
42
|
+
2. **`unread` flag instead of soft-delete or status enum.**
|
|
43
|
+
- Fast count queries for badge (`SELECT COUNT(*) WHERE recipient_session_id = ? AND unread = 1`).
|
|
44
|
+
- Unread state is mutable; allows mark-as-read UX without data loss.
|
|
45
|
+
- Simplifies archive/export (no tombstone filtering needed).
|
|
46
|
+
|
|
47
|
+
3. **`sender_type` as an explicit column.**
|
|
48
|
+
- Allows schema validation (e.g., "agent" senders must have valid IDs; "system" is always valid).
|
|
49
|
+
- Enables filtered views ("show only agent messages" or "system alerts only") without string parsing.
|
|
50
|
+
|
|
51
|
+
4. **`interaction_id` for grouping.**
|
|
52
|
+
- Enables undo/redo on related messages (e.g., canceling all build-failure notifications from one dispatch).
|
|
53
|
+
- Supports consolidation: multiple small notifications can be collapsed into one summary message.
|
|
54
|
+
|
|
55
|
+
5. **Persistent `sender_name` at write-time.**
|
|
56
|
+
- Operator may rename agents or disable them; storing the name ensures historical accuracy.
|
|
57
|
+
- UI doesn't need to perform runtime lookups on archived inboxes.
|
|
58
|
+
|
|
59
|
+
6. **Unix timestamps (milliseconds) instead of ISO strings.**
|
|
60
|
+
- Enables range queries (`sent_at BETWEEN ? AND ?`) without parsing.
|
|
61
|
+
- Smaller column footprint; easier to sort.
|
|
62
|
+
- Downside: not human-readable; UI must convert to locale strings.
|
|
63
|
+
|
|
64
|
+
### Entity Relationships
|
|
65
|
+
|
|
66
|
+
- **inbox_entries ← dispatch WI feedback:** A failed dispatch may create multiple inbox entries (error summary, retry recommendation, logs snippet). Threaded via `interaction_id`.
|
|
67
|
+
- **inbox_entries ← PR reviews:** Humans and auto-review agents post comments; summarized as inbox alerts to project owners via `interaction_id` per-PR.
|
|
68
|
+
- **inbox_entries ← consolidation:** `engine/consolidation.js` reads inbox entries to extract `agent:` frontmatter and route findings into per-agent knowledge files.
|
|
69
|
+
|
|
70
|
+
## Performance Considerations
|
|
71
|
+
|
|
72
|
+
### Indexing Strategy
|
|
73
|
+
|
|
74
|
+
**Current indexes (recommended):**
|
|
75
|
+
- `idx_recipient_sent` on `(recipient_session_id, sent_at DESC)` — enables efficient "unread count" and "latest N messages" queries without table scans.
|
|
76
|
+
- `idx_interaction` on `(interaction_id)` — enables grouping/dedup queries (e.g., "was this interaction already notified?").
|
|
77
|
+
- PK `id` is implicit index.
|
|
78
|
+
|
|
79
|
+
**Query patterns:**
|
|
80
|
+
- **Unread count per session:** `SELECT COUNT(*) WHERE recipient_session_id = ? AND unread = 1` → must scan `idx_recipient_sent` to avoid full table.
|
|
81
|
+
- **Latest messages:** `SELECT * WHERE recipient_session_id = ? ORDER BY sent_at DESC LIMIT 20` → seeks on `idx_recipient_sent` DESC.
|
|
82
|
+
- **Thread view:** `SELECT * WHERE interaction_id = ? ORDER BY sent_at` → seeks on `idx_interaction`, then sorts in application or via secondary index.
|
|
83
|
+
- **Cleanup:** `DELETE WHERE sent_at < ? AND unread = 0` → full table scan (acceptable; cleanup is infrequent, low-priority background task).
|
|
84
|
+
|
|
85
|
+
**Missing indexes (future optimization):**
|
|
86
|
+
- `idx_sender_id` — if queries like "messages from this agent" are common, consider adding.
|
|
87
|
+
- `idx_unread` — if badge ("N unread") is a hot query, consider combining with recipient (`idx_recipient_unread` on `(recipient_session_id, unread)`).
|
|
88
|
+
|
|
89
|
+
### Size Estimates & Cleanup
|
|
90
|
+
|
|
91
|
+
- **Row size:** ~500–2000 bytes (summary + content; content can be verbose with logs/code snippets).
|
|
92
|
+
- **Growth:** ~5–50 entries/day (depends on agent activity and verbose flagging).
|
|
93
|
+
- **Retention:** No automatic cleanup; operators may archive/export and manually delete aged entries. Engine does NOT auto-prune.
|
|
94
|
+
- **Recommended**: Implement a dashboard setting to soft-delete entries older than N days (default 30) with an undo/export option.
|
|
95
|
+
|
|
96
|
+
## Migration & Backward Compatibility
|
|
97
|
+
|
|
98
|
+
- **Migration 018 or later** creates `inbox_entries` as part of SQL state cutover. Legacy `inbox/` Markdown files are NOT imported; operators must re-send or manually recreate critical messages.
|
|
99
|
+
- **No schema versioning needed** for now; table is immutable-append-only (reads/writes only).
|
|
100
|
+
|
|
101
|
+
## Related Code
|
|
102
|
+
|
|
103
|
+
- **Writes:** `engine/inbox-store.js` (primary mutator; uses `getDb()` and `emitStateEvent`).
|
|
104
|
+
- **Reads:** `engine/queries.js#getInboxForSession()`, dashboard `/api/inbox`, `engine/consolidation.js`.
|
|
105
|
+
- **UI:** `dashboard/pages/inbox.html`, `dashboard/js/inbox.js`.
|
|
106
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// engine/db/migrations/019-inbox-entries.js
|
|
2
|
+
//
|
|
3
|
+
// Creates the `inbox_entries` table: persistent store for operator and agent
|
|
4
|
+
// inbox messages, decoupled from the runtime work-item/dispatch lifecycle.
|
|
5
|
+
//
|
|
6
|
+
// Schema: columns, types, constraints, and indexing strategy documented in
|
|
7
|
+
// docs/design-inbox-entries-schema.md
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
version: 19,
|
|
11
|
+
description: 'inbox_entries: operator/agent message queue with full schema + indexes',
|
|
12
|
+
up(db) {
|
|
13
|
+
db.exec(`
|
|
14
|
+
CREATE TABLE inbox_entries (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
recipient_session_id TEXT NOT NULL,
|
|
17
|
+
sender_id TEXT NOT NULL,
|
|
18
|
+
sender_name TEXT NOT NULL,
|
|
19
|
+
sender_type TEXT NOT NULL,
|
|
20
|
+
interaction_id TEXT NOT NULL,
|
|
21
|
+
sequence INTEGER NOT NULL DEFAULT 0,
|
|
22
|
+
summary TEXT NOT NULL,
|
|
23
|
+
content TEXT NOT NULL,
|
|
24
|
+
unread INTEGER NOT NULL DEFAULT 1,
|
|
25
|
+
sent_at INTEGER NOT NULL,
|
|
26
|
+
read_at INTEGER,
|
|
27
|
+
notified_at INTEGER
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE INDEX idx_inbox_recipient_sent
|
|
31
|
+
ON inbox_entries(recipient_session_id, sent_at DESC);
|
|
32
|
+
|
|
33
|
+
CREATE INDEX idx_inbox_interaction
|
|
34
|
+
ON inbox_entries(interaction_id);
|
|
35
|
+
`);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// engine/inbox-store.js — SQL-backed inbox_entries table handlers.
|
|
2
|
+
//
|
|
3
|
+
// Provides read/write helpers for the operator and agent inbox message queue.
|
|
4
|
+
// Inboxes are decoupled from the runtime work-item/dispatch lifecycle and
|
|
5
|
+
// support threading via interaction_id, unread tracking, and time-ordered display.
|
|
6
|
+
//
|
|
7
|
+
// Schema: docs/design-inbox-entries-schema.md
|
|
8
|
+
|
|
9
|
+
const crypto = require('node:crypto');
|
|
10
|
+
const { getDb, withTransaction } = require('./db');
|
|
11
|
+
|
|
12
|
+
// In-process monotonic counter. Millisecond timestamps are too coarse to
|
|
13
|
+
// disambiguate bursts of createInboxEntry() calls for the same
|
|
14
|
+
// recipient+sender pair (e.g. rapid consolidation/notification writes), and
|
|
15
|
+
// the caller-supplied `sequence` field is not guaranteed to be unique either
|
|
16
|
+
// (it defaults to 0 and callers are not required to increment it). The
|
|
17
|
+
// counter guarantees every id generated within this process is distinct even
|
|
18
|
+
// when Date.now() does not advance between calls.
|
|
19
|
+
let _inboxIdCounter = 0;
|
|
20
|
+
|
|
21
|
+
// Generate a likely-unique inbox entry ID:
|
|
22
|
+
// <recipient>-<timestamp>-<sender>-<seq>-<counter>-<random>
|
|
23
|
+
// `sequence` is preserved for callers that pass a meaningful value; the
|
|
24
|
+
// monotonic counter + random suffix are what actually make the id
|
|
25
|
+
// collision-proof, independent of clock resolution or caller behavior.
|
|
26
|
+
function _generateInboxId(recipientSessionId, senderId, sequence) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
_inboxIdCounter = (_inboxIdCounter + 1) % 0x1000000;
|
|
29
|
+
const rand = crypto.randomBytes(4).toString('hex');
|
|
30
|
+
return `${recipientSessionId}-${now}-${senderId}-${sequence}-${_inboxIdCounter.toString(36)}-${rand}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Read all inbox entries for a given recipient session, ordered by time descending.
|
|
34
|
+
function _readInboxEntriesFromSql(db, { recipientSessionId } = {}) {
|
|
35
|
+
let query = 'SELECT * FROM inbox_entries';
|
|
36
|
+
const params = [];
|
|
37
|
+
if (recipientSessionId) {
|
|
38
|
+
query += ' WHERE recipient_session_id = ?';
|
|
39
|
+
params.push(recipientSessionId);
|
|
40
|
+
}
|
|
41
|
+
query += ' ORDER BY sent_at DESC';
|
|
42
|
+
return db.prepare(query).all(...params);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Read unread count for a recipient.
|
|
46
|
+
function getInboxUnreadCount(recipientSessionId) {
|
|
47
|
+
let db;
|
|
48
|
+
try { db = getDb(); }
|
|
49
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
50
|
+
const row = db.prepare('SELECT COUNT(*) AS count FROM inbox_entries WHERE recipient_session_id = ? AND unread = 1')
|
|
51
|
+
.get(recipientSessionId);
|
|
52
|
+
return row ? row.count : 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Read all inbox entries for a recipient session.
|
|
56
|
+
function getInboxForSession(recipientSessionId, { limit = null, offset = 0 } = {}) {
|
|
57
|
+
let db;
|
|
58
|
+
try { db = getDb(); }
|
|
59
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
60
|
+
let query = 'SELECT * FROM inbox_entries WHERE recipient_session_id = ? ORDER BY sent_at DESC';
|
|
61
|
+
const params = [recipientSessionId];
|
|
62
|
+
if (limit) {
|
|
63
|
+
query += ' LIMIT ? OFFSET ?';
|
|
64
|
+
params.push(limit, offset);
|
|
65
|
+
}
|
|
66
|
+
return db.prepare(query).all(...params);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Read a single inbox entry by ID.
|
|
70
|
+
function getInboxEntryById(id) {
|
|
71
|
+
let db;
|
|
72
|
+
try { db = getDb(); }
|
|
73
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
74
|
+
return db.prepare('SELECT * FROM inbox_entries WHERE id = ?').get(id);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create a new inbox entry. Returns the entry object.
|
|
78
|
+
function createInboxEntry({
|
|
79
|
+
recipientSessionId,
|
|
80
|
+
senderId,
|
|
81
|
+
senderName,
|
|
82
|
+
senderType,
|
|
83
|
+
interactionId,
|
|
84
|
+
summary,
|
|
85
|
+
content,
|
|
86
|
+
sequence = 0,
|
|
87
|
+
}) {
|
|
88
|
+
if (!recipientSessionId || !senderId || !senderName || !senderType || !interactionId || !summary || !content) {
|
|
89
|
+
throw new Error('inbox-store: missing required fields for createInboxEntry');
|
|
90
|
+
}
|
|
91
|
+
if (!['agent', 'system', 'operator'].includes(senderType)) {
|
|
92
|
+
throw new Error(`inbox-store: invalid senderType "${senderType}"`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let db;
|
|
96
|
+
try { db = getDb(); }
|
|
97
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
98
|
+
|
|
99
|
+
const MAX_ID_COLLISION_RETRIES = 5;
|
|
100
|
+
|
|
101
|
+
return withTransaction(db, () => {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const insert = db.prepare(`
|
|
104
|
+
INSERT INTO inbox_entries
|
|
105
|
+
(id, recipient_session_id, sender_id, sender_name, sender_type, interaction_id,
|
|
106
|
+
sequence, summary, content, unread, sent_at, read_at, notified_at)
|
|
107
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, NULL, NULL)
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
// _generateInboxId() is collision-proof in practice (monotonic
|
|
111
|
+
// per-process counter + random suffix), but retry-on-conflict is kept as
|
|
112
|
+
// defense-in-depth against an id collision from any other source (e.g. a
|
|
113
|
+
// pre-existing row imported/replayed from another process) rather than
|
|
114
|
+
// letting a rare UNIQUE-constraint failure crash the caller.
|
|
115
|
+
let id;
|
|
116
|
+
for (let attempt = 0; attempt < MAX_ID_COLLISION_RETRIES; attempt++) {
|
|
117
|
+
id = _generateInboxId(recipientSessionId, senderId, sequence);
|
|
118
|
+
try {
|
|
119
|
+
insert.run(id, recipientSessionId, senderId, senderName, senderType, interactionId, sequence, summary, content, now);
|
|
120
|
+
break;
|
|
121
|
+
} catch (e) {
|
|
122
|
+
const isUniqueViolation = e && (e.code === 'ERR_SQLITE_ERROR' || e.errcode === 1555 || /UNIQUE constraint failed/.test(e.message || ''));
|
|
123
|
+
if (!isUniqueViolation || attempt === MAX_ID_COLLISION_RETRIES - 1) throw e;
|
|
124
|
+
// Regenerate and retry — the monotonic counter guarantees the next
|
|
125
|
+
// attempt produces a different id.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return getInboxEntryById(id);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Mark one or more inbox entries as read.
|
|
134
|
+
function markInboxEntriesAsRead(entryIds) {
|
|
135
|
+
if (!Array.isArray(entryIds) || !entryIds.length) return { wrote: false };
|
|
136
|
+
|
|
137
|
+
let db;
|
|
138
|
+
try { db = getDb(); }
|
|
139
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
140
|
+
|
|
141
|
+
return withTransaction(db, () => {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const placeholders = entryIds.map(() => '?').join(',');
|
|
144
|
+
const query = `UPDATE inbox_entries SET unread = 0, read_at = ? WHERE id IN (${placeholders})`;
|
|
145
|
+
const stmt = db.prepare(query);
|
|
146
|
+
const result = stmt.run(now, ...entryIds);
|
|
147
|
+
return { wrote: result.changes > 0 };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Mark one or more inbox entries as unread.
|
|
152
|
+
function markInboxEntriesAsUnread(entryIds) {
|
|
153
|
+
if (!Array.isArray(entryIds) || !entryIds.length) return { wrote: false };
|
|
154
|
+
|
|
155
|
+
let db;
|
|
156
|
+
try { db = getDb(); }
|
|
157
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
158
|
+
|
|
159
|
+
return withTransaction(db, () => {
|
|
160
|
+
const placeholders = entryIds.map(() => '?').join(',');
|
|
161
|
+
const query = `UPDATE inbox_entries SET unread = 1, read_at = NULL WHERE id IN (${placeholders})`;
|
|
162
|
+
const stmt = db.prepare(query);
|
|
163
|
+
const result = stmt.run(...entryIds);
|
|
164
|
+
return { wrote: result.changes > 0 };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Mark all inbox entries for a recipient as read.
|
|
169
|
+
function markAllInboxAsRead(recipientSessionId) {
|
|
170
|
+
let db;
|
|
171
|
+
try { db = getDb(); }
|
|
172
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
173
|
+
|
|
174
|
+
return withTransaction(db, () => {
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
const result = db.prepare(
|
|
177
|
+
'UPDATE inbox_entries SET unread = 0, read_at = ? WHERE recipient_session_id = ? AND unread = 1'
|
|
178
|
+
).run(now, recipientSessionId);
|
|
179
|
+
return { wrote: result.changes > 0 };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Delete an inbox entry by ID.
|
|
184
|
+
function deleteInboxEntry(id) {
|
|
185
|
+
let db;
|
|
186
|
+
try { db = getDb(); }
|
|
187
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
188
|
+
|
|
189
|
+
return withTransaction(db, () => {
|
|
190
|
+
const result = db.prepare('DELETE FROM inbox_entries WHERE id = ?').run(id);
|
|
191
|
+
return { wrote: result.changes > 0 };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Delete multiple inbox entries by ID.
|
|
196
|
+
function deleteInboxEntries(entryIds) {
|
|
197
|
+
if (!Array.isArray(entryIds) || !entryIds.length) return { wrote: false };
|
|
198
|
+
|
|
199
|
+
let db;
|
|
200
|
+
try { db = getDb(); }
|
|
201
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
202
|
+
|
|
203
|
+
return withTransaction(db, () => {
|
|
204
|
+
const placeholders = entryIds.map(() => '?').join(',');
|
|
205
|
+
const query = `DELETE FROM inbox_entries WHERE id IN (${placeholders})`;
|
|
206
|
+
const result = db.prepare(query).run(...entryIds);
|
|
207
|
+
return { wrote: result.changes > 0 };
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Delete all unread entries older than a given timestamp (in milliseconds).
|
|
212
|
+
// Used for cleanup/archival.
|
|
213
|
+
function deleteInboxEntriesOlderThan(timestampMs, { unreadOnly = false } = {}) {
|
|
214
|
+
let db;
|
|
215
|
+
try { db = getDb(); }
|
|
216
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
217
|
+
|
|
218
|
+
return withTransaction(db, () => {
|
|
219
|
+
let query = 'DELETE FROM inbox_entries WHERE sent_at < ?';
|
|
220
|
+
const params = [timestampMs];
|
|
221
|
+
if (unreadOnly) {
|
|
222
|
+
query += ' AND unread = 1';
|
|
223
|
+
}
|
|
224
|
+
const result = db.prepare(query).run(...params);
|
|
225
|
+
return { wrote: result.changes > 0, deletedCount: result.changes };
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Retrieve inbox entries grouped by interaction_id.
|
|
230
|
+
function getInboxByInteraction(interactionId) {
|
|
231
|
+
let db;
|
|
232
|
+
try { db = getDb(); }
|
|
233
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
234
|
+
return db.prepare('SELECT * FROM inbox_entries WHERE interaction_id = ? ORDER BY sent_at ASC')
|
|
235
|
+
.all(interactionId);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Retrieve all unread entries for all recipients.
|
|
239
|
+
function getAllUnreadInboxEntries() {
|
|
240
|
+
let db;
|
|
241
|
+
try { db = getDb(); }
|
|
242
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
243
|
+
return db.prepare('SELECT * FROM inbox_entries WHERE unread = 1 ORDER BY sent_at DESC').all();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Update the notified_at timestamp for an entry (marks when a notification was sent).
|
|
247
|
+
function markInboxEntryNotified(id) {
|
|
248
|
+
let db;
|
|
249
|
+
try { db = getDb(); }
|
|
250
|
+
catch (e) { throw new Error(`inbox-store: SQLite unavailable (${e.message})`); }
|
|
251
|
+
|
|
252
|
+
return withTransaction(db, () => {
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
const result = db.prepare('UPDATE inbox_entries SET notified_at = ? WHERE id = ?').run(now, id);
|
|
255
|
+
return { wrote: result.changes > 0 };
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
// Read operations
|
|
261
|
+
getInboxUnreadCount,
|
|
262
|
+
getInboxForSession,
|
|
263
|
+
getInboxEntryById,
|
|
264
|
+
getInboxByInteraction,
|
|
265
|
+
getAllUnreadInboxEntries,
|
|
266
|
+
|
|
267
|
+
// Write operations
|
|
268
|
+
createInboxEntry,
|
|
269
|
+
markInboxEntriesAsRead,
|
|
270
|
+
markInboxEntriesAsUnread,
|
|
271
|
+
markAllInboxAsRead,
|
|
272
|
+
markInboxEntryNotified,
|
|
273
|
+
deleteInboxEntry,
|
|
274
|
+
deleteInboxEntries,
|
|
275
|
+
deleteInboxEntriesOlderThan,
|
|
276
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2374",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|