@geekbeer/minion 2.68.6 → 2.73.0
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/core/api.js +28 -29
- package/core/db.js +297 -0
- package/core/lib/thread-watcher.js +24 -15
- package/core/routes/daily-logs.js +17 -11
- package/core/routes/memory.js +16 -10
- package/core/routes/{help-threads.js → threads.js} +30 -30
- package/core/stores/chat-store.js +138 -76
- package/core/stores/daily-log-store.js +84 -105
- package/core/stores/execution-store.js +59 -84
- package/core/stores/memory-store.js +84 -177
- package/core/stores/routine-store.js +55 -72
- package/core/stores/workflow-store.js +52 -69
- package/docs/api-reference.md +34 -31
- package/linux/routes/chat.js +54 -7
- package/linux/server.js +5 -3
- package/package.json +2 -1
- package/roles/engineer.md +12 -0
- package/roles/pm.md +16 -0
- package/rules/core.md +66 -0
- package/win/routes/chat.js +54 -7
- package/win/server.js +4 -2
package/core/api.js
CHANGED
|
@@ -88,75 +88,75 @@ async function sendHeartbeat(data) {
|
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* Create a project thread (help or discussion) on HQ.
|
|
91
|
-
* @param {object} data - { project_id, title,
|
|
91
|
+
* @param {object} data - { project_id, title, content, thread_type?, mentions?, context? }
|
|
92
92
|
* @returns {Promise<{ thread: object }>}
|
|
93
93
|
*/
|
|
94
|
-
async function
|
|
95
|
-
return request('/
|
|
94
|
+
async function createThread(data) {
|
|
95
|
+
return request('/threads', {
|
|
96
96
|
method: 'POST',
|
|
97
97
|
body: JSON.stringify(data),
|
|
98
98
|
})
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Get open
|
|
102
|
+
* Get open threads in this minion's projects.
|
|
103
103
|
* @returns {Promise<{ threads: object[] }>}
|
|
104
104
|
*/
|
|
105
|
-
async function
|
|
106
|
-
return request('/
|
|
105
|
+
async function getOpenThreads() {
|
|
106
|
+
return request('/threads/open')
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Get a
|
|
110
|
+
* Get a thread by ID with messages.
|
|
111
111
|
* @param {string} threadId
|
|
112
112
|
* @returns {Promise<{ thread: object, messages: object[] }>}
|
|
113
113
|
*/
|
|
114
|
-
async function
|
|
115
|
-
return request(`/
|
|
114
|
+
async function getThread(threadId) {
|
|
115
|
+
return request(`/threads/${threadId}`)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Post a message to a
|
|
119
|
+
* Post a message to a thread.
|
|
120
120
|
* @param {string} threadId
|
|
121
121
|
* @param {object} data - { content, attachments?, mentions? }
|
|
122
122
|
*/
|
|
123
|
-
async function
|
|
124
|
-
return request(`/
|
|
123
|
+
async function postThreadMessage(threadId, data) {
|
|
124
|
+
return request(`/threads/${threadId}/messages`, {
|
|
125
125
|
method: 'POST',
|
|
126
126
|
body: JSON.stringify(data),
|
|
127
127
|
})
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* Resolve a
|
|
131
|
+
* Resolve a thread.
|
|
132
132
|
* @param {string} threadId
|
|
133
133
|
* @param {string} resolution
|
|
134
134
|
*/
|
|
135
|
-
async function
|
|
136
|
-
return request(`/
|
|
135
|
+
async function resolveThread(threadId, resolution) {
|
|
136
|
+
return request(`/threads/${threadId}/resolve`, {
|
|
137
137
|
method: 'PATCH',
|
|
138
138
|
body: JSON.stringify({ resolution }),
|
|
139
139
|
})
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
* Cancel a
|
|
143
|
+
* Cancel a thread.
|
|
144
144
|
* @param {string} threadId
|
|
145
145
|
* @param {string} [reason]
|
|
146
146
|
*/
|
|
147
|
-
async function
|
|
148
|
-
return request(`/
|
|
147
|
+
async function cancelThread(threadId, reason) {
|
|
148
|
+
return request(`/threads/${threadId}/cancel`, {
|
|
149
149
|
method: 'PATCH',
|
|
150
150
|
body: JSON.stringify({ reason: reason || null }),
|
|
151
151
|
})
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
* Permanently delete a
|
|
155
|
+
* Permanently delete a thread (PM only).
|
|
156
156
|
* @param {string} threadId
|
|
157
157
|
*/
|
|
158
|
-
async function
|
|
159
|
-
return request(`/
|
|
158
|
+
async function deleteThread(threadId) {
|
|
159
|
+
return request(`/threads/${threadId}`, {
|
|
160
160
|
method: 'DELETE',
|
|
161
161
|
})
|
|
162
162
|
}
|
|
@@ -192,14 +192,13 @@ module.exports = {
|
|
|
192
192
|
reportStepComplete,
|
|
193
193
|
reportIssue,
|
|
194
194
|
sendHeartbeat,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
deleteHelpThread,
|
|
195
|
+
createThread,
|
|
196
|
+
getOpenThreads,
|
|
197
|
+
getThread,
|
|
198
|
+
postThreadMessage,
|
|
199
|
+
resolveThread,
|
|
200
|
+
cancelThread,
|
|
201
|
+
deleteThread,
|
|
203
202
|
createProjectMemory,
|
|
204
203
|
searchProjectMemories,
|
|
205
204
|
}
|
package/core/db.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Database Module
|
|
3
|
+
* Provides a singleton SQLite database connection for all minion stores.
|
|
4
|
+
* Uses better-sqlite3 (synchronous API) with WAL mode for performance.
|
|
5
|
+
*
|
|
6
|
+
* Database file: $DATA_DIR/minion.db
|
|
7
|
+
*
|
|
8
|
+
* FTS5 uses the trigram tokenizer for Japanese/CJK language support.
|
|
9
|
+
* Trigram splits text into 3-character sequences, enabling substring search
|
|
10
|
+
* in any language without a language-specific tokenizer.
|
|
11
|
+
* Note: trigram requires search queries of 3+ characters.
|
|
12
|
+
* Shorter queries fall back to LIKE-based search in the store layer.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path')
|
|
16
|
+
const fs = require('fs')
|
|
17
|
+
|
|
18
|
+
const { DATA_DIR } = require('./lib/platform')
|
|
19
|
+
const { config } = require('./config')
|
|
20
|
+
|
|
21
|
+
let _db = null
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the database file path.
|
|
25
|
+
* Uses DATA_DIR if accessible, otherwise falls back to HOME_DIR.
|
|
26
|
+
*/
|
|
27
|
+
function resolveDbPath() {
|
|
28
|
+
try {
|
|
29
|
+
fs.accessSync(DATA_DIR, fs.constants.W_OK)
|
|
30
|
+
return path.join(DATA_DIR, 'minion.db')
|
|
31
|
+
} catch {
|
|
32
|
+
return path.join(config.HOME_DIR, 'minion.db')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the singleton database connection.
|
|
38
|
+
* Initializes the database and schema on first call.
|
|
39
|
+
* @returns {import('better-sqlite3').Database}
|
|
40
|
+
*/
|
|
41
|
+
function getDb() {
|
|
42
|
+
if (_db) return _db
|
|
43
|
+
|
|
44
|
+
const dbPath = resolveDbPath()
|
|
45
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true })
|
|
46
|
+
|
|
47
|
+
const Database = require('better-sqlite3')
|
|
48
|
+
_db = new Database(dbPath)
|
|
49
|
+
|
|
50
|
+
// Performance & integrity settings
|
|
51
|
+
_db.pragma('journal_mode = WAL')
|
|
52
|
+
_db.pragma('foreign_keys = ON')
|
|
53
|
+
|
|
54
|
+
initSchema(_db)
|
|
55
|
+
migrateSchema(_db)
|
|
56
|
+
|
|
57
|
+
console.log(`[DB] SQLite database initialized: ${dbPath}`)
|
|
58
|
+
return _db
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Initialize all tables, indices, FTS virtual tables, and triggers.
|
|
63
|
+
* All statements use IF NOT EXISTS for idempotency.
|
|
64
|
+
*/
|
|
65
|
+
function initSchema(db) {
|
|
66
|
+
db.exec(`
|
|
67
|
+
-- ==================== memories ====================
|
|
68
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
title TEXT NOT NULL DEFAULT 'Untitled',
|
|
71
|
+
category TEXT NOT NULL DEFAULT 'reference'
|
|
72
|
+
CHECK (category IN ('user', 'feedback', 'project', 'reference')),
|
|
73
|
+
content TEXT NOT NULL DEFAULT '',
|
|
74
|
+
created_at TEXT NOT NULL,
|
|
75
|
+
updated_at TEXT NOT NULL
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at DESC);
|
|
80
|
+
|
|
81
|
+
-- FTS5 with trigram tokenizer for Japanese/CJK support
|
|
82
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
83
|
+
title,
|
|
84
|
+
content,
|
|
85
|
+
content=memories,
|
|
86
|
+
content_rowid=rowid,
|
|
87
|
+
tokenize='trigram'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
-- Triggers to keep FTS index in sync with memories table
|
|
91
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
92
|
+
INSERT INTO memories_fts(rowid, title, content)
|
|
93
|
+
VALUES (new.rowid, new.title, new.content);
|
|
94
|
+
END;
|
|
95
|
+
|
|
96
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
97
|
+
INSERT INTO memories_fts(memories_fts, rowid, title, content)
|
|
98
|
+
VALUES ('delete', old.rowid, old.title, old.content);
|
|
99
|
+
END;
|
|
100
|
+
|
|
101
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
102
|
+
INSERT INTO memories_fts(memories_fts, rowid, title, content)
|
|
103
|
+
VALUES ('delete', old.rowid, old.title, old.content);
|
|
104
|
+
INSERT INTO memories_fts(rowid, title, content)
|
|
105
|
+
VALUES (new.rowid, new.title, new.content);
|
|
106
|
+
END;
|
|
107
|
+
|
|
108
|
+
-- ==================== daily_logs ====================
|
|
109
|
+
CREATE TABLE IF NOT EXISTS daily_logs (
|
|
110
|
+
date TEXT PRIMARY KEY,
|
|
111
|
+
content TEXT NOT NULL DEFAULT '',
|
|
112
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
113
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
-- FTS5 with trigram tokenizer for Japanese/CJK support
|
|
117
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS daily_logs_fts USING fts5(
|
|
118
|
+
content,
|
|
119
|
+
content=daily_logs,
|
|
120
|
+
content_rowid=rowid,
|
|
121
|
+
tokenize='trigram'
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
CREATE TRIGGER IF NOT EXISTS daily_logs_ai AFTER INSERT ON daily_logs BEGIN
|
|
125
|
+
INSERT INTO daily_logs_fts(rowid, content)
|
|
126
|
+
VALUES (new.rowid, new.content);
|
|
127
|
+
END;
|
|
128
|
+
|
|
129
|
+
CREATE TRIGGER IF NOT EXISTS daily_logs_ad AFTER DELETE ON daily_logs BEGIN
|
|
130
|
+
INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
|
|
131
|
+
VALUES ('delete', old.rowid, old.content);
|
|
132
|
+
END;
|
|
133
|
+
|
|
134
|
+
CREATE TRIGGER IF NOT EXISTS daily_logs_au AFTER UPDATE ON daily_logs BEGIN
|
|
135
|
+
INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
|
|
136
|
+
VALUES ('delete', old.rowid, old.content);
|
|
137
|
+
INSERT INTO daily_logs_fts(rowid, content)
|
|
138
|
+
VALUES (new.rowid, new.content);
|
|
139
|
+
END;
|
|
140
|
+
|
|
141
|
+
-- ==================== chat_sessions ====================
|
|
142
|
+
CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
143
|
+
session_id TEXT PRIMARY KEY,
|
|
144
|
+
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
145
|
+
created_at INTEGER NOT NULL,
|
|
146
|
+
updated_at INTEGER NOT NULL
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
150
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
151
|
+
session_id TEXT NOT NULL REFERENCES chat_sessions(session_id) ON DELETE CASCADE,
|
|
152
|
+
role TEXT NOT NULL,
|
|
153
|
+
content TEXT NOT NULL,
|
|
154
|
+
timestamp INTEGER NOT NULL
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id, id);
|
|
158
|
+
|
|
159
|
+
-- ==================== executions ====================
|
|
160
|
+
CREATE TABLE IF NOT EXISTS executions (
|
|
161
|
+
id TEXT PRIMARY KEY,
|
|
162
|
+
skill_name TEXT,
|
|
163
|
+
workflow_id TEXT,
|
|
164
|
+
status TEXT,
|
|
165
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
166
|
+
data TEXT NOT NULL
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_executions_workflow ON executions(workflow_id);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_executions_created ON executions(created_at DESC);
|
|
171
|
+
|
|
172
|
+
-- ==================== routines ====================
|
|
173
|
+
CREATE TABLE IF NOT EXISTS routines (
|
|
174
|
+
id TEXT PRIMARY KEY,
|
|
175
|
+
name TEXT NOT NULL,
|
|
176
|
+
data TEXT NOT NULL
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_routines_name ON routines(name);
|
|
180
|
+
|
|
181
|
+
-- ==================== workflows ====================
|
|
182
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
183
|
+
id TEXT PRIMARY KEY,
|
|
184
|
+
name TEXT NOT NULL,
|
|
185
|
+
data TEXT NOT NULL
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_workflows_name ON workflows(name);
|
|
189
|
+
|
|
190
|
+
-- ==================== schema_version ====================
|
|
191
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
192
|
+
version INTEGER PRIMARY KEY,
|
|
193
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
194
|
+
);
|
|
195
|
+
`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Run schema migrations for existing databases.
|
|
200
|
+
* Each migration checks schema_version to avoid re-running.
|
|
201
|
+
*/
|
|
202
|
+
function migrateSchema(db) {
|
|
203
|
+
const currentVersion = db.prepare(
|
|
204
|
+
'SELECT COALESCE(MAX(version), 0) as v FROM schema_version'
|
|
205
|
+
).get().v
|
|
206
|
+
|
|
207
|
+
if (currentVersion < 1) {
|
|
208
|
+
// Migration 1: Recreate FTS tables with trigram tokenizer for Japanese support.
|
|
209
|
+
// The default unicode61 tokenizer doesn't tokenize CJK text correctly.
|
|
210
|
+
console.log('[DB] Migration 1: Switching FTS5 to trigram tokenizer...')
|
|
211
|
+
|
|
212
|
+
db.exec(`
|
|
213
|
+
-- Drop old FTS tables and triggers (may use unicode61 tokenizer)
|
|
214
|
+
DROP TRIGGER IF EXISTS memories_ai;
|
|
215
|
+
DROP TRIGGER IF EXISTS memories_ad;
|
|
216
|
+
DROP TRIGGER IF EXISTS memories_au;
|
|
217
|
+
DROP TABLE IF EXISTS memories_fts;
|
|
218
|
+
|
|
219
|
+
DROP TRIGGER IF EXISTS daily_logs_ai;
|
|
220
|
+
DROP TRIGGER IF EXISTS daily_logs_ad;
|
|
221
|
+
DROP TRIGGER IF EXISTS daily_logs_au;
|
|
222
|
+
DROP TABLE IF EXISTS daily_logs_fts;
|
|
223
|
+
|
|
224
|
+
-- Recreate with trigram tokenizer
|
|
225
|
+
CREATE VIRTUAL TABLE memories_fts USING fts5(
|
|
226
|
+
title,
|
|
227
|
+
content,
|
|
228
|
+
content=memories,
|
|
229
|
+
content_rowid=rowid,
|
|
230
|
+
tokenize='trigram'
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
|
|
234
|
+
INSERT INTO memories_fts(rowid, title, content)
|
|
235
|
+
VALUES (new.rowid, new.title, new.content);
|
|
236
|
+
END;
|
|
237
|
+
|
|
238
|
+
CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN
|
|
239
|
+
INSERT INTO memories_fts(memories_fts, rowid, title, content)
|
|
240
|
+
VALUES ('delete', old.rowid, old.title, old.content);
|
|
241
|
+
END;
|
|
242
|
+
|
|
243
|
+
CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN
|
|
244
|
+
INSERT INTO memories_fts(memories_fts, rowid, title, content)
|
|
245
|
+
VALUES ('delete', old.rowid, old.title, old.content);
|
|
246
|
+
INSERT INTO memories_fts(rowid, title, content)
|
|
247
|
+
VALUES (new.rowid, new.title, new.content);
|
|
248
|
+
END;
|
|
249
|
+
|
|
250
|
+
CREATE VIRTUAL TABLE daily_logs_fts USING fts5(
|
|
251
|
+
content,
|
|
252
|
+
content=daily_logs,
|
|
253
|
+
content_rowid=rowid,
|
|
254
|
+
tokenize='trigram'
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
CREATE TRIGGER daily_logs_ai AFTER INSERT ON daily_logs BEGIN
|
|
258
|
+
INSERT INTO daily_logs_fts(rowid, content)
|
|
259
|
+
VALUES (new.rowid, new.content);
|
|
260
|
+
END;
|
|
261
|
+
|
|
262
|
+
CREATE TRIGGER daily_logs_ad AFTER DELETE ON daily_logs BEGIN
|
|
263
|
+
INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
|
|
264
|
+
VALUES ('delete', old.rowid, old.content);
|
|
265
|
+
END;
|
|
266
|
+
|
|
267
|
+
CREATE TRIGGER daily_logs_au AFTER UPDATE ON daily_logs BEGIN
|
|
268
|
+
INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
|
|
269
|
+
VALUES ('delete', old.rowid, old.content);
|
|
270
|
+
INSERT INTO daily_logs_fts(rowid, content)
|
|
271
|
+
VALUES (new.rowid, new.content);
|
|
272
|
+
END;
|
|
273
|
+
|
|
274
|
+
-- Rebuild FTS index from existing data
|
|
275
|
+
INSERT INTO memories_fts(memories_fts) VALUES ('rebuild');
|
|
276
|
+
INSERT INTO daily_logs_fts(daily_logs_fts) VALUES ('rebuild');
|
|
277
|
+
|
|
278
|
+
INSERT INTO schema_version (version) VALUES (1);
|
|
279
|
+
`)
|
|
280
|
+
|
|
281
|
+
console.log('[DB] Migration 1 complete: FTS5 trigram tokenizer applied')
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Close the database connection.
|
|
287
|
+
* Call this during graceful shutdown to flush WAL.
|
|
288
|
+
*/
|
|
289
|
+
function closeDb() {
|
|
290
|
+
if (_db) {
|
|
291
|
+
_db.close()
|
|
292
|
+
_db = null
|
|
293
|
+
console.log('[DB] SQLite database closed')
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = { getDb, closeDb }
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 3. Rate limiting: max 1 LLM evaluation per thread per 5 minutes
|
|
11
11
|
*
|
|
12
12
|
* Flow per poll cycle:
|
|
13
|
-
* 1. GET /api/minion/
|
|
13
|
+
* 1. GET /api/minion/threads/open → list of open threads
|
|
14
14
|
* 2. For each thread with new activity:
|
|
15
15
|
* a. Check mentions → if mentioned, must evaluate
|
|
16
16
|
* b. Check read state → skip if no new messages
|
|
@@ -98,7 +98,7 @@ async function pollOnce() {
|
|
|
98
98
|
|
|
99
99
|
polling = true
|
|
100
100
|
try {
|
|
101
|
-
const data = await api.request('/
|
|
101
|
+
const data = await api.request('/threads/open')
|
|
102
102
|
|
|
103
103
|
if (!data.threads || data.threads.length === 0) {
|
|
104
104
|
return
|
|
@@ -139,7 +139,7 @@ async function processThread(thread, myProjects, now) {
|
|
|
139
139
|
// Fetch thread detail to get message count and check for new messages
|
|
140
140
|
let detail
|
|
141
141
|
try {
|
|
142
|
-
detail = await api.request(`/
|
|
142
|
+
detail = await api.request(`/threads/${thread.id}`)
|
|
143
143
|
} catch {
|
|
144
144
|
return
|
|
145
145
|
}
|
|
@@ -155,6 +155,13 @@ async function processThread(thread, myProjects, now) {
|
|
|
155
155
|
// New messages detected
|
|
156
156
|
const newMessages = messages.slice(state.lastMessageCount)
|
|
157
157
|
|
|
158
|
+
// Skip if all new messages are from self (avoid re-triggering on own posts)
|
|
159
|
+
const newFromOthers = newMessages.filter(m => m.sender_minion_id !== config.MINION_ID)
|
|
160
|
+
if (newFromOthers.length === 0) {
|
|
161
|
+
readState.set(thread.id, { ...state, lastMessageCount: messageCount })
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
158
165
|
// Update read state (message count only — eval timestamp updated after LLM call)
|
|
159
166
|
readState.set(thread.id, { ...state, lastMessageCount: messageCount })
|
|
160
167
|
|
|
@@ -187,16 +194,16 @@ async function evaluateWithLlm(threadSummary, threadDetail, allMessages, newMess
|
|
|
187
194
|
const messageHistory = recentMessages
|
|
188
195
|
.map(m => {
|
|
189
196
|
const sender = m.sender_minion_id
|
|
190
|
-
? (m.sender_minion_id === config.MINION_ID ? 'You' : `Minion(${m.sender_minion_id.slice(0, 8)})`)
|
|
191
|
-
: 'User'
|
|
197
|
+
? (m.sender_minion_id === config.MINION_ID ? 'You' : (m.sender_name || `Minion(${m.sender_minion_id.slice(0, 8)})`))
|
|
198
|
+
: (m.sender_name || 'User')
|
|
192
199
|
return `[${sender}] ${m.content}`
|
|
193
200
|
})
|
|
194
201
|
.join('\n')
|
|
195
202
|
|
|
196
203
|
const threadType = threadDetail.thread_type || 'help'
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
const isRequester = threadDetail.requester_minion_id === config.MINION_ID
|
|
205
|
+
const mentions = threadDetail.mentions || []
|
|
206
|
+
const mentionsSummary = mentions.length > 0 ? mentions.join(', ') : 'なし(全員に向けた投稿)'
|
|
200
207
|
|
|
201
208
|
// Extract optional context metadata
|
|
202
209
|
const ctx = threadDetail.context || {}
|
|
@@ -205,13 +212,14 @@ async function evaluateWithLlm(threadSummary, threadDetail, allMessages, newMess
|
|
|
205
212
|
ctx.attempted_resolution ? `試行済み: ${ctx.attempted_resolution}` : '',
|
|
206
213
|
].filter(Boolean).join('\n')
|
|
207
214
|
|
|
208
|
-
const prompt = `あなたはプロジェクト「${myProject.name}」のチームメンバー(ロール: ${myProject.role})です。
|
|
215
|
+
const prompt = `あなたはプロジェクト「${myProject.name}」のチームメンバー(ロール: ${myProject.role}、ID: ${config.MINION_ID})です。
|
|
209
216
|
以下のスレッドに対して、あなたが返信すべきかどうかを判断し、返信する場合はその内容を生成してください。
|
|
210
217
|
|
|
211
218
|
スレッドタイプ: ${threadType}
|
|
212
219
|
タイトル: ${threadDetail.title}
|
|
213
|
-
|
|
214
|
-
${
|
|
220
|
+
起票者: ${isRequester ? 'あなた自身' : `他のミニオン(${threadDetail.requester_minion_id?.slice(0, 8)})`}
|
|
221
|
+
メンション対象: ${mentionsSummary}
|
|
222
|
+
${contextInfo}
|
|
215
223
|
|
|
216
224
|
--- メッセージ履歴 ---
|
|
217
225
|
${messageHistory || '(メッセージなし)'}
|
|
@@ -225,10 +233,11 @@ ${messageHistory || '(メッセージなし)'}
|
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
判断基準:
|
|
236
|
+
- 自分が起票したスレッドの場合、他のメンバーの回答を待つべき(追加情報がある場合を除く)
|
|
237
|
+
- メンション対象が特定のロールやミニオンに限定されている場合、自分が対象でなければ静観する
|
|
228
238
|
- 自分のロール(${myProject.role})に関連する話題か
|
|
229
239
|
- 自分が貢献できる知見や意見があるか
|
|
230
240
|
- 既に十分な回答がある場合は重複を避ける
|
|
231
|
-
- メンションされている場合は必ず返信する
|
|
232
241
|
- 人間に聞くべき場合は @user メンションを含めて返信する`
|
|
233
242
|
|
|
234
243
|
try {
|
|
@@ -251,7 +260,7 @@ ${messageHistory || '(メッセージなし)'}
|
|
|
251
260
|
mentions.push('user')
|
|
252
261
|
}
|
|
253
262
|
|
|
254
|
-
await api.request(`/
|
|
263
|
+
await api.request(`/threads/${threadSummary.id}/messages`, {
|
|
255
264
|
method: 'POST',
|
|
256
265
|
body: JSON.stringify({
|
|
257
266
|
content: parsed.response,
|
|
@@ -281,7 +290,7 @@ async function fallbackMemoryMatch(thread) {
|
|
|
281
290
|
|
|
282
291
|
if (!memData.memories || memData.memories.length === 0) return
|
|
283
292
|
|
|
284
|
-
const keywords =
|
|
293
|
+
const keywords = thread.title
|
|
285
294
|
.toLowerCase()
|
|
286
295
|
.split(/[\s,.\-_/]+/)
|
|
287
296
|
.filter(w => w.length >= 3)
|
|
@@ -301,7 +310,7 @@ async function fallbackMemoryMatch(thread) {
|
|
|
301
310
|
.map(m => `[${m.title}] ${m.content}`)
|
|
302
311
|
.join('\n\n')
|
|
303
312
|
|
|
304
|
-
await api.request(`/
|
|
313
|
+
await api.request(`/threads/${thread.id}/messages`, {
|
|
305
314
|
method: 'POST',
|
|
306
315
|
body: JSON.stringify({
|
|
307
316
|
content: `関連するプロジェクトメモリーが見つかりました:\n\n${knowledgeSummary}`,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Daily log endpoints
|
|
3
3
|
*
|
|
4
|
-
* Provides CRUD
|
|
5
|
-
* Logs are stored
|
|
4
|
+
* Provides CRUD + search for daily conversation summaries.
|
|
5
|
+
* Logs are stored in SQLite with FTS5 full-text search.
|
|
6
6
|
*
|
|
7
7
|
* Endpoints:
|
|
8
|
-
* GET /api/daily-logs - List all logs (
|
|
8
|
+
* GET /api/daily-logs - List all logs (or search with ?search=keyword)
|
|
9
9
|
* POST /api/daily-logs - Create a new daily log
|
|
10
10
|
* GET /api/daily-logs/:date - Get a specific day's log
|
|
11
11
|
* PUT /api/daily-logs/:date - Update a daily log
|
|
@@ -21,14 +21,20 @@ const dailyLogStore = require('../stores/daily-log-store')
|
|
|
21
21
|
*/
|
|
22
22
|
async function dailyLogRoutes(fastify) {
|
|
23
23
|
|
|
24
|
-
// GET /api/daily-logs - List
|
|
24
|
+
// GET /api/daily-logs - List or search logs
|
|
25
25
|
fastify.get('/api/daily-logs', async (request, reply) => {
|
|
26
26
|
if (!verifyToken(request)) {
|
|
27
27
|
reply.code(401)
|
|
28
28
|
return { success: false, error: 'Unauthorized' }
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const { search } = request.query || {}
|
|
32
|
+
if (search) {
|
|
33
|
+
const logs = dailyLogStore.searchLogs(search)
|
|
34
|
+
return { success: true, logs }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const logs = dailyLogStore.listLogs()
|
|
32
38
|
return { success: true, logs }
|
|
33
39
|
})
|
|
34
40
|
|
|
@@ -46,13 +52,13 @@ async function dailyLogRoutes(fastify) {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
// Check if log already exists
|
|
49
|
-
const existing =
|
|
55
|
+
const existing = dailyLogStore.loadLog(date)
|
|
50
56
|
if (existing !== null) {
|
|
51
57
|
reply.code(409)
|
|
52
58
|
return { success: false, error: 'Log already exists for this date. Use PUT to update.' }
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
dailyLogStore.saveLog(date, content)
|
|
56
62
|
console.log(`[DailyLogs] Created log: ${date}`)
|
|
57
63
|
return { success: true, log: { date, content } }
|
|
58
64
|
})
|
|
@@ -65,7 +71,7 @@ async function dailyLogRoutes(fastify) {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
const { date } = request.params
|
|
68
|
-
const content =
|
|
74
|
+
const content = dailyLogStore.loadLog(date)
|
|
69
75
|
if (content === null) {
|
|
70
76
|
reply.code(404)
|
|
71
77
|
return { success: false, error: 'Log not found' }
|
|
@@ -89,13 +95,13 @@ async function dailyLogRoutes(fastify) {
|
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
// Check if log exists
|
|
92
|
-
const existing =
|
|
98
|
+
const existing = dailyLogStore.loadLog(date)
|
|
93
99
|
if (existing === null) {
|
|
94
100
|
reply.code(404)
|
|
95
101
|
return { success: false, error: 'Log not found' }
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
|
|
104
|
+
dailyLogStore.saveLog(date, content)
|
|
99
105
|
console.log(`[DailyLogs] Updated log: ${date}`)
|
|
100
106
|
return { success: true, log: { date, content } }
|
|
101
107
|
})
|
|
@@ -108,7 +114,7 @@ async function dailyLogRoutes(fastify) {
|
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
const { date } = request.params
|
|
111
|
-
const deleted =
|
|
117
|
+
const deleted = dailyLogStore.deleteLog(date)
|
|
112
118
|
if (!deleted) {
|
|
113
119
|
reply.code(404)
|
|
114
120
|
return { success: false, error: 'Log not found' }
|