@geekbeer/minion 3.11.5 → 3.14.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/db.js +71 -40
- package/core/stores/chat-store.js +57 -21
- package/docs/api-reference.md +27 -2
- package/docs/environment-setup.md +9 -0
- package/linux/routes/chat.js +55 -34
- package/package.json +1 -1
- package/rules/core.md +24 -0
- package/rules/windows.md +72 -0
- package/win/routes/chat.js +52 -30
- package/win/routes/terminal.js +14 -1
- package/win/server.js +6 -4
- package/win/wsl-session-server.js +45 -0
package/core/db.js
CHANGED
|
@@ -195,6 +195,7 @@ function initSchema(db) {
|
|
|
195
195
|
-- ==================== chat_sessions ====================
|
|
196
196
|
CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
197
197
|
session_id TEXT PRIMARY KEY,
|
|
198
|
+
workspace_id TEXT,
|
|
198
199
|
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
199
200
|
created_at INTEGER NOT NULL,
|
|
200
201
|
updated_at INTEGER NOT NULL
|
|
@@ -390,57 +391,87 @@ function migrateSchema(db) {
|
|
|
390
391
|
}
|
|
391
392
|
|
|
392
393
|
if (currentVersion < 2) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
INSERT
|
|
406
|
-
|
|
407
|
-
|
|
394
|
+
try {
|
|
395
|
+
console.log('[DB] Migration 2: Adding emails FTS5...')
|
|
396
|
+
|
|
397
|
+
db.exec(`
|
|
398
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS emails_fts USING fts5(
|
|
399
|
+
subject,
|
|
400
|
+
body_text,
|
|
401
|
+
content=emails,
|
|
402
|
+
content_rowid=rowid,
|
|
403
|
+
tokenize='trigram'
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
CREATE TRIGGER IF NOT EXISTS emails_ai AFTER INSERT ON emails BEGIN
|
|
407
|
+
INSERT INTO emails_fts(rowid, subject, body_text)
|
|
408
|
+
VALUES (new.rowid, new.subject, new.body_text);
|
|
409
|
+
END;
|
|
410
|
+
|
|
411
|
+
CREATE TRIGGER IF NOT EXISTS emails_ad AFTER DELETE ON emails BEGIN
|
|
412
|
+
INSERT INTO emails_fts(emails_fts, rowid, subject, body_text)
|
|
413
|
+
VALUES ('delete', old.rowid, old.subject, old.body_text);
|
|
414
|
+
END;
|
|
415
|
+
|
|
416
|
+
CREATE TRIGGER IF NOT EXISTS emails_au AFTER UPDATE ON emails BEGIN
|
|
417
|
+
INSERT INTO emails_fts(emails_fts, rowid, subject, body_text)
|
|
418
|
+
VALUES ('delete', old.rowid, old.subject, old.body_text);
|
|
419
|
+
INSERT INTO emails_fts(rowid, subject, body_text)
|
|
420
|
+
VALUES (new.rowid, new.subject, new.body_text);
|
|
421
|
+
END;
|
|
422
|
+
|
|
423
|
+
INSERT INTO emails_fts(emails_fts) VALUES ('rebuild');
|
|
424
|
+
|
|
425
|
+
INSERT INTO schema_version (version) VALUES (2);
|
|
426
|
+
`)
|
|
427
|
+
|
|
428
|
+
console.log('[DB] Migration 2 complete: emails FTS5 added')
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.warn(`[DB] Migration 2 skipped (emails table may not exist yet): ${err.message}`)
|
|
431
|
+
// Mark as applied so it doesn't block subsequent migrations.
|
|
432
|
+
// emails FTS will be created by initSchema on next fresh DB.
|
|
433
|
+
try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (2)") } catch {}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
408
436
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
END;
|
|
437
|
+
if (currentVersion < 3) {
|
|
438
|
+
try {
|
|
439
|
+
console.log('[DB] Migration 3: Adding project_id to memories...')
|
|
413
440
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
VALUES ('delete', old.rowid, old.subject, old.body_text);
|
|
417
|
-
INSERT INTO emails_fts(rowid, subject, body_text)
|
|
418
|
-
VALUES (new.rowid, new.subject, new.body_text);
|
|
419
|
-
END;
|
|
441
|
+
db.exec(`
|
|
442
|
+
ALTER TABLE memories ADD COLUMN project_id TEXT DEFAULT NULL;
|
|
420
443
|
|
|
421
|
-
|
|
444
|
+
CREATE INDEX IF NOT EXISTS idx_memories_project_id ON memories(project_id);
|
|
422
445
|
|
|
423
|
-
|
|
424
|
-
|
|
446
|
+
INSERT INTO schema_version (version) VALUES (3);
|
|
447
|
+
`)
|
|
425
448
|
|
|
426
|
-
|
|
449
|
+
console.log('[DB] Migration 3 complete: memories.project_id added')
|
|
450
|
+
} catch (err) {
|
|
451
|
+
// Column may already exist (duplicate column error)
|
|
452
|
+
console.warn(`[DB] Migration 3 skipped: ${err.message}`)
|
|
453
|
+
try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (3)") } catch {}
|
|
454
|
+
}
|
|
427
455
|
}
|
|
428
456
|
|
|
429
|
-
if (currentVersion <
|
|
430
|
-
|
|
457
|
+
if (currentVersion < 4) {
|
|
458
|
+
try {
|
|
459
|
+
console.log('[DB] Migration 4: Adding workspace_id to chat_sessions...')
|
|
431
460
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
-- NULL = universal memory (cross-project knowledge).
|
|
435
|
-
-- Set = project-scoped memory.
|
|
436
|
-
ALTER TABLE memories ADD COLUMN project_id TEXT DEFAULT NULL;
|
|
461
|
+
db.exec(`
|
|
462
|
+
ALTER TABLE chat_sessions ADD COLUMN workspace_id TEXT DEFAULT NULL;
|
|
437
463
|
|
|
438
|
-
|
|
464
|
+
CREATE INDEX IF NOT EXISTS idx_chat_sessions_workspace
|
|
465
|
+
ON chat_sessions(workspace_id, updated_at DESC);
|
|
439
466
|
|
|
440
|
-
|
|
441
|
-
|
|
467
|
+
INSERT INTO schema_version (version) VALUES (4);
|
|
468
|
+
`)
|
|
442
469
|
|
|
443
|
-
|
|
470
|
+
console.log('[DB] Migration 4 complete: chat_sessions.workspace_id added')
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.warn(`[DB] Migration 4 skipped: ${err.message}`)
|
|
473
|
+
try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (4)") } catch {}
|
|
474
|
+
}
|
|
444
475
|
}
|
|
445
476
|
}
|
|
446
477
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chat Session Store (SQLite)
|
|
3
3
|
* Persists chat sessions and messages.
|
|
4
|
-
* Supports multiple sessions
|
|
4
|
+
* Supports multiple sessions scoped by workspace_id.
|
|
5
5
|
* Claude CLI manages conversation context via --resume.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -10,14 +10,22 @@ const { getDb } = require('../db')
|
|
|
10
10
|
const MAX_MESSAGES = 100
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Load the active (most recent) chat session
|
|
13
|
+
* Load the active (most recent) chat session for a workspace.
|
|
14
|
+
* @param {string|null} workspaceId - Workspace ID to scope sessions
|
|
14
15
|
* @returns {object|null} Session object or null if none exists
|
|
15
16
|
*/
|
|
16
|
-
function load() {
|
|
17
|
+
function load(workspaceId) {
|
|
17
18
|
const db = getDb()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
let session
|
|
20
|
+
if (workspaceId) {
|
|
21
|
+
session = db.prepare(
|
|
22
|
+
'SELECT * FROM chat_sessions WHERE workspace_id = ? ORDER BY updated_at DESC LIMIT 1'
|
|
23
|
+
).get(workspaceId)
|
|
24
|
+
} else {
|
|
25
|
+
session = db.prepare(
|
|
26
|
+
'SELECT * FROM chat_sessions WHERE workspace_id IS NULL ORDER BY updated_at DESC LIMIT 1'
|
|
27
|
+
).get()
|
|
28
|
+
}
|
|
21
29
|
if (!session) return null
|
|
22
30
|
|
|
23
31
|
const messages = db.prepare(
|
|
@@ -26,6 +34,7 @@ function load() {
|
|
|
26
34
|
|
|
27
35
|
return {
|
|
28
36
|
session_id: session.session_id,
|
|
37
|
+
workspace_id: session.workspace_id,
|
|
29
38
|
messages,
|
|
30
39
|
turn_count: session.turn_count,
|
|
31
40
|
created_at: session.created_at,
|
|
@@ -49,6 +58,7 @@ function loadById(sessionId) {
|
|
|
49
58
|
|
|
50
59
|
return {
|
|
51
60
|
session_id: session.session_id,
|
|
61
|
+
workspace_id: session.workspace_id,
|
|
52
62
|
messages,
|
|
53
63
|
turn_count: session.turn_count,
|
|
54
64
|
created_at: session.created_at,
|
|
@@ -57,15 +67,32 @@ function loadById(sessionId) {
|
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/**
|
|
60
|
-
* List
|
|
70
|
+
* List sessions, optionally filtered by workspace_id.
|
|
61
71
|
* @param {number} limit
|
|
62
|
-
* @
|
|
72
|
+
* @param {string|null} workspaceId - If provided, filter by workspace
|
|
73
|
+
* @returns {Array<{ session_id, workspace_id, turn_count, message_count, created_at, updated_at }>}
|
|
63
74
|
*/
|
|
64
|
-
function listSessions(limit = 50) {
|
|
75
|
+
function listSessions(limit = 50, workspaceId) {
|
|
65
76
|
const db = getDb()
|
|
77
|
+
if (workspaceId) {
|
|
78
|
+
return db.prepare(`
|
|
79
|
+
SELECT
|
|
80
|
+
s.session_id,
|
|
81
|
+
s.workspace_id,
|
|
82
|
+
s.turn_count,
|
|
83
|
+
s.created_at,
|
|
84
|
+
s.updated_at,
|
|
85
|
+
(SELECT COUNT(*) FROM chat_messages WHERE session_id = s.session_id) as message_count
|
|
86
|
+
FROM chat_sessions s
|
|
87
|
+
WHERE s.workspace_id = ?
|
|
88
|
+
ORDER BY s.updated_at DESC
|
|
89
|
+
LIMIT ?
|
|
90
|
+
`).all(workspaceId, limit)
|
|
91
|
+
}
|
|
66
92
|
return db.prepare(`
|
|
67
93
|
SELECT
|
|
68
94
|
s.session_id,
|
|
95
|
+
s.workspace_id,
|
|
69
96
|
s.turn_count,
|
|
70
97
|
s.created_at,
|
|
71
98
|
s.updated_at,
|
|
@@ -88,8 +115,8 @@ function save(session) {
|
|
|
88
115
|
|
|
89
116
|
// Insert session
|
|
90
117
|
db.prepare(
|
|
91
|
-
'INSERT INTO chat_sessions (session_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?)'
|
|
92
|
-
).run(session.session_id, session.turn_count || 0, session.created_at, session.updated_at)
|
|
118
|
+
'INSERT INTO chat_sessions (session_id, workspace_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)'
|
|
119
|
+
).run(session.session_id, session.workspace_id || null, session.turn_count || 0, session.created_at, session.updated_at)
|
|
93
120
|
|
|
94
121
|
// Insert messages
|
|
95
122
|
const insertMsg = db.prepare(
|
|
@@ -103,14 +130,15 @@ function save(session) {
|
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
/**
|
|
106
|
-
* Add a message to
|
|
107
|
-
* Creates a new session if none exists
|
|
133
|
+
* Add a message to a session.
|
|
134
|
+
* Creates a new session if none exists for the given session_id.
|
|
108
135
|
* Past sessions are preserved (not deleted).
|
|
109
136
|
* @param {string} sessionId - Claude CLI session ID
|
|
110
137
|
* @param {{ role: string, content: string }} msg - Message to add
|
|
111
138
|
* @param {number} [turnCount] - Optional turn count to add
|
|
139
|
+
* @param {string|null} [workspaceId] - Workspace ID for new sessions
|
|
112
140
|
*/
|
|
113
|
-
function addMessage(sessionId, msg, turnCount) {
|
|
141
|
+
function addMessage(sessionId, msg, turnCount, workspaceId) {
|
|
114
142
|
const db = getDb()
|
|
115
143
|
const existing = db.prepare('SELECT * FROM chat_sessions WHERE session_id = ?').get(sessionId)
|
|
116
144
|
|
|
@@ -118,8 +146,8 @@ function addMessage(sessionId, msg, turnCount) {
|
|
|
118
146
|
if (!existing) {
|
|
119
147
|
const now = Date.now()
|
|
120
148
|
db.prepare(
|
|
121
|
-
'INSERT INTO chat_sessions (session_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?)'
|
|
122
|
-
).run(sessionId, 0, now, now)
|
|
149
|
+
'INSERT INTO chat_sessions (session_id, workspace_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)'
|
|
150
|
+
).run(sessionId, workspaceId || null, 0, now, now)
|
|
123
151
|
}
|
|
124
152
|
|
|
125
153
|
// Insert message
|
|
@@ -149,13 +177,21 @@ function addMessage(sessionId, msg, turnCount) {
|
|
|
149
177
|
}
|
|
150
178
|
|
|
151
179
|
/**
|
|
152
|
-
* Clear the active session
|
|
153
|
-
*
|
|
154
|
-
* For reset flows, use clearActive() which preserves the session in history.
|
|
180
|
+
* Clear the active session for a workspace.
|
|
181
|
+
* @param {string|null} workspaceId - Workspace to clear session for
|
|
155
182
|
*/
|
|
156
|
-
function clear() {
|
|
183
|
+
function clear(workspaceId) {
|
|
157
184
|
const db = getDb()
|
|
158
|
-
|
|
185
|
+
let active
|
|
186
|
+
if (workspaceId) {
|
|
187
|
+
active = db.prepare(
|
|
188
|
+
'SELECT session_id FROM chat_sessions WHERE workspace_id = ? ORDER BY updated_at DESC LIMIT 1'
|
|
189
|
+
).get(workspaceId)
|
|
190
|
+
} else {
|
|
191
|
+
active = db.prepare(
|
|
192
|
+
'SELECT session_id FROM chat_sessions WHERE workspace_id IS NULL ORDER BY updated_at DESC LIMIT 1'
|
|
193
|
+
).get()
|
|
194
|
+
}
|
|
159
195
|
if (active) {
|
|
160
196
|
db.prepare('DELETE FROM chat_sessions WHERE session_id = ?').run(active.session_id)
|
|
161
197
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -54,9 +54,9 @@ Outcome values: `success`, `failure`, `partial`
|
|
|
54
54
|
|
|
55
55
|
| Method | Endpoint | Description |
|
|
56
56
|
|--------|----------|-------------|
|
|
57
|
-
| GET | `/api/terminal/sessions` | List
|
|
57
|
+
| GET | `/api/terminal/sessions` | List sessions (CMD + WSL merged) |
|
|
58
58
|
| POST | `/api/terminal/send` | Send keys. Body: `{session, input?, enter?, special?}` |
|
|
59
|
-
| POST | `/api/terminal/create` | Create session. Body: `{name?, command?}` |
|
|
59
|
+
| POST | `/api/terminal/create` | Create session. Body: `{name?, command?, type?, get_or_create?}` |
|
|
60
60
|
| POST | `/api/terminal/kill` | Kill session. Body: `{session}` |
|
|
61
61
|
| GET | `/api/terminal/capture` | Capture pane content. Query: `?session=&lines=100` |
|
|
62
62
|
| GET | `/api/terminal/ttyd/status` | ttyd process status |
|
|
@@ -64,6 +64,31 @@ Outcome values: `success`, `failure`, `partial`
|
|
|
64
64
|
| POST | `/api/terminal/ttyd/stop` | Stop ttyd for session. Body: `{session}` |
|
|
65
65
|
| POST | `/api/terminal/ttyd/stop-all` | Stop all ttyd processes |
|
|
66
66
|
|
|
67
|
+
#### WSL セッション(Windows ミニオン限定)
|
|
68
|
+
|
|
69
|
+
WSL 内でコマンドを実行するには `type: "wsl"` を指定する。セッション名は `wsl-` prefix が自動付与される。
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# WSL セッション作成(既存があれば再利用)
|
|
73
|
+
curl -X POST http://localhost:8080/api/terminal/create \
|
|
74
|
+
-H "Authorization: Bearer $API_TOKEN" \
|
|
75
|
+
-H "Content-Type: application/json" \
|
|
76
|
+
-d '{"name": "wsl-dev", "type": "wsl", "get_or_create": true}'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| パラメータ | 型 | 説明 |
|
|
80
|
+
|-----------|-----|------|
|
|
81
|
+
| `type` | string | `"wsl"` で WSL セッションを作成 |
|
|
82
|
+
| `get_or_create` | boolean | `true` の場合、同名の既存セッションがあれば再利用(レスポンスに `reused: true`) |
|
|
83
|
+
|
|
84
|
+
WSL セッションの上限は **5つ**。上限に達すると `429` を返す。不要なセッションを `kill` してから再作成すること。
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# WSL サーバー稼働状態の確認
|
|
88
|
+
curl http://localhost:8080/api/terminal/wsl/status \
|
|
89
|
+
-H "Authorization: Bearer $API_TOKEN"
|
|
90
|
+
```
|
|
91
|
+
|
|
67
92
|
### Files
|
|
68
93
|
|
|
69
94
|
Files are stored in `~/files/`. Max upload size: 50MB.
|
|
@@ -232,6 +232,14 @@ curl -X POST http://localhost:8080/api/chat \
|
|
|
232
232
|
| 7682 | WSL session server (HTTP API) | localhost のみ |
|
|
233
233
|
| 7683 | WSL session server (WebSocket) | localhost のみ、ttyd プロトコル |
|
|
234
234
|
|
|
235
|
+
### セッション管理
|
|
236
|
+
|
|
237
|
+
- **セッション再利用**: `get_or_create: true` を指定すると、同名の既存セッションがあれば再利用される。毎回新しいセッションを作らないこと。
|
|
238
|
+
- **固定名**: `wsl-dev`, `wsl-build` など目的を示す名前を使う。名前を省略するとタイムスタンプ名が生成され再利用できない。
|
|
239
|
+
- **上限**: WSL セッションは最大 5 つ。上限に達すると作成が拒否される。
|
|
240
|
+
- **自動クリーンアップ**: 完了済みセッションは 5 分後に自動削除される。
|
|
241
|
+
- **手動クリーンアップ**: 不要なセッションは `POST /api/terminal/kill` で終了する。
|
|
242
|
+
|
|
235
243
|
### トラブルシューティング
|
|
236
244
|
|
|
237
245
|
| 問題 | 原因 | 対処 |
|
|
@@ -239,6 +247,7 @@ curl -X POST http://localhost:8080/api/chat \
|
|
|
239
247
|
| WSL session server is not running | ユーザーが未ログイン | RDP/コンソールでログイン |
|
|
240
248
|
| WSL not detected during setup | WSL 未インストール | `wsl --install` を実行後 `minion-cli setup` を再実行 |
|
|
241
249
|
| Connection refused on port 7682 | サーバー異常終了 | `schtasks /Run /TN "MinionWSL"` で再起動 |
|
|
250
|
+
| WSL session limit reached | 5 セッション上限 | `GET /api/terminal/sessions` で確認し不要なセッションを `kill` |
|
|
242
251
|
|
|
243
252
|
---
|
|
244
253
|
|
package/linux/routes/chat.js
CHANGED
|
@@ -41,20 +41,22 @@ async function chatRoutes(fastify) {
|
|
|
41
41
|
return { success: false, error: 'Unauthorized' }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const { message, session_id, context } = request.body || {}
|
|
44
|
+
const { message, session_id, context, workspace_id } = request.body || {}
|
|
45
45
|
|
|
46
46
|
if (!message || typeof message !== 'string') {
|
|
47
47
|
reply.code(400)
|
|
48
48
|
return { success: false, error: 'message is required' }
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
const workspaceId = workspace_id || null
|
|
52
|
+
|
|
51
53
|
// Build prompt — add memory context on new sessions + page context
|
|
52
54
|
const prompt = await buildContextPrefix(message, context, session_id)
|
|
53
55
|
|
|
54
56
|
// Store user message
|
|
55
57
|
const currentSessionId = session_id || null
|
|
56
58
|
if (currentSessionId) {
|
|
57
|
-
await chatStore.addMessage(currentSessionId, { role: 'user', content: message })
|
|
59
|
+
await chatStore.addMessage(currentSessionId, { role: 'user', content: message }, undefined, workspaceId)
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
// Take over response handling from Fastify for SSE streaming
|
|
@@ -68,7 +70,7 @@ async function chatRoutes(fastify) {
|
|
|
68
70
|
reply.raw.flushHeaders()
|
|
69
71
|
|
|
70
72
|
try {
|
|
71
|
-
await streamLlmResponse(reply.raw, prompt, currentSessionId)
|
|
73
|
+
await streamLlmResponse(reply.raw, prompt, currentSessionId, workspaceId, message)
|
|
72
74
|
} catch (err) {
|
|
73
75
|
console.error('[Chat] stream error:', err.message)
|
|
74
76
|
const errorEvent = JSON.stringify({ type: 'error', error: err.message })
|
|
@@ -78,14 +80,15 @@ async function chatRoutes(fastify) {
|
|
|
78
80
|
reply.raw.end()
|
|
79
81
|
})
|
|
80
82
|
|
|
81
|
-
// GET /api/chat/session - Get active chat session
|
|
83
|
+
// GET /api/chat/session - Get active chat session for a workspace
|
|
82
84
|
fastify.get('/api/chat/session', async (request, reply) => {
|
|
83
85
|
if (!verifyToken(request)) {
|
|
84
86
|
reply.code(401)
|
|
85
87
|
return { success: false, error: 'Unauthorized' }
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
const
|
|
90
|
+
const workspaceId = request.query?.workspace_id || null
|
|
91
|
+
const session = chatStore.load(workspaceId)
|
|
89
92
|
if (!session) {
|
|
90
93
|
return { success: true, session: null }
|
|
91
94
|
}
|
|
@@ -94,6 +97,7 @@ async function chatRoutes(fastify) {
|
|
|
94
97
|
success: true,
|
|
95
98
|
session: {
|
|
96
99
|
session_id: session.session_id,
|
|
100
|
+
workspace_id: session.workspace_id,
|
|
97
101
|
messages: session.messages,
|
|
98
102
|
turn_count: session.turn_count,
|
|
99
103
|
created_at: session.created_at,
|
|
@@ -102,7 +106,7 @@ async function chatRoutes(fastify) {
|
|
|
102
106
|
}
|
|
103
107
|
})
|
|
104
108
|
|
|
105
|
-
// GET /api/chat/sessions - List
|
|
109
|
+
// GET /api/chat/sessions - List sessions, optionally filtered by workspace
|
|
106
110
|
fastify.get('/api/chat/sessions', async (request, reply) => {
|
|
107
111
|
if (!verifyToken(request)) {
|
|
108
112
|
reply.code(401)
|
|
@@ -110,7 +114,8 @@ async function chatRoutes(fastify) {
|
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
const limit = parseInt(request.query?.limit) || 50
|
|
113
|
-
const
|
|
117
|
+
const workspaceId = request.query?.workspace_id || undefined
|
|
118
|
+
const sessions = chatStore.listSessions(limit, workspaceId)
|
|
114
119
|
return { success: true, sessions }
|
|
115
120
|
})
|
|
116
121
|
|
|
@@ -130,14 +135,15 @@ async function chatRoutes(fastify) {
|
|
|
130
135
|
return { success: true, session }
|
|
131
136
|
})
|
|
132
137
|
|
|
133
|
-
// POST /api/chat/clear - Clear the active session
|
|
138
|
+
// POST /api/chat/clear - Clear the active session for a workspace
|
|
134
139
|
fastify.post('/api/chat/clear', async (request, reply) => {
|
|
135
140
|
if (!verifyToken(request)) {
|
|
136
141
|
reply.code(401)
|
|
137
142
|
return { success: false, error: 'Unauthorized' }
|
|
138
143
|
}
|
|
139
144
|
|
|
140
|
-
|
|
145
|
+
const workspaceId = request.body?.workspace_id || null
|
|
146
|
+
await chatStore.clear(workspaceId)
|
|
141
147
|
return { success: true }
|
|
142
148
|
})
|
|
143
149
|
|
|
@@ -168,42 +174,57 @@ async function chatRoutes(fastify) {
|
|
|
168
174
|
return { success: true }
|
|
169
175
|
})
|
|
170
176
|
|
|
171
|
-
// POST /api/chat/reset -
|
|
177
|
+
// POST /api/chat/reset - Carry over relevant messages and start fresh session
|
|
172
178
|
fastify.post('/api/chat/reset', async (request, reply) => {
|
|
173
179
|
if (!verifyToken(request)) {
|
|
174
180
|
reply.code(401)
|
|
175
181
|
return { success: false, error: 'Unauthorized' }
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
const
|
|
184
|
+
const workspaceId = request.body?.workspace_id || null
|
|
185
|
+
const session = await chatStore.load(workspaceId)
|
|
179
186
|
if (!session || session.messages.length === 0) {
|
|
180
|
-
await chatStore.clear()
|
|
181
|
-
return { success: true,
|
|
187
|
+
await chatStore.clear(workspaceId)
|
|
188
|
+
return { success: true, carry_over: null }
|
|
182
189
|
}
|
|
183
190
|
|
|
184
|
-
//
|
|
191
|
+
// Take recent messages for topic analysis
|
|
185
192
|
const recentMessages = session.messages.slice(-20)
|
|
186
|
-
|
|
187
|
-
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 500)}`)
|
|
188
|
-
.join('\n\n')
|
|
189
|
-
|
|
190
|
-
const summarizePrompt = `以下の会話を要約してください。重要なコンテキスト、決定事項、進行中のタスクを含めてください。200文字以内で。\n\n${conversationText}`
|
|
193
|
+
let carryOverMessages = recentMessages
|
|
191
194
|
|
|
192
|
-
|
|
195
|
+
// Ask LLM to find where the current topic begins
|
|
193
196
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
console.error('[Chat] summarization failed:', err.message)
|
|
197
|
-
// Fallback: use last few messages as summary
|
|
198
|
-
const fallback = recentMessages.slice(-4)
|
|
199
|
-
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 200)}`)
|
|
197
|
+
const indexed = recentMessages
|
|
198
|
+
.map((m, i) => `[${i}] ${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 300)}`)
|
|
200
199
|
.join('\n')
|
|
201
|
-
|
|
200
|
+
|
|
201
|
+
const analyzePrompt = `以下の会話履歴を分析し、現在進行中のトピックが始まったインデックス番号を特定してください。
|
|
202
|
+
会話の流れが明確に変わったポイント(新しい質問・別の作業への切り替え等)がある場合、最後の切り替え以降のインデックスを返してください。
|
|
203
|
+
全体が同じトピックの場合は 0 を返してください。
|
|
204
|
+
|
|
205
|
+
数値のみを返してください(例: 5)。
|
|
206
|
+
|
|
207
|
+
${indexed}`
|
|
208
|
+
|
|
209
|
+
const result = await runQuickLlmCall(analyzePrompt)
|
|
210
|
+
const breakIndex = parseInt(result.trim(), 10)
|
|
211
|
+
if (!isNaN(breakIndex) && breakIndex >= 0 && breakIndex < recentMessages.length) {
|
|
212
|
+
carryOverMessages = recentMessages.slice(breakIndex)
|
|
213
|
+
console.log(`[Chat] topic break at index ${breakIndex}, carrying over ${carryOverMessages.length}/${recentMessages.length} messages`)
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error('[Chat] topic analysis failed, carrying over last 10:', err.message)
|
|
217
|
+
carryOverMessages = recentMessages.slice(-10)
|
|
202
218
|
}
|
|
203
219
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
220
|
+
// Format carry-over as conversation text
|
|
221
|
+
const carryOver = carryOverMessages
|
|
222
|
+
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content}`)
|
|
223
|
+
.join('\n\n')
|
|
224
|
+
|
|
225
|
+
await chatStore.clear(workspaceId)
|
|
226
|
+
console.log(`[Chat] session reset, carrying over ${carryOverMessages.length} messages (${carryOver.length} chars)`)
|
|
227
|
+
return { success: true, carry_over: carryOver }
|
|
207
228
|
})
|
|
208
229
|
|
|
209
230
|
// POST /api/chat/end-of-day - Generate daily log + extract memories
|
|
@@ -341,7 +362,7 @@ function getLlmBinary() {
|
|
|
341
362
|
* Tracks block types to correctly forward tool_use vs text events
|
|
342
363
|
* and counts turns for session management.
|
|
343
364
|
*/
|
|
344
|
-
function streamLlmResponse(res, prompt, sessionId) {
|
|
365
|
+
function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage) {
|
|
345
366
|
return new Promise((resolve, reject) => {
|
|
346
367
|
const binaryName = getLlmBinary()
|
|
347
368
|
if (!binaryName) {
|
|
@@ -516,11 +537,11 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
516
537
|
if (resolvedSessionId) {
|
|
517
538
|
// If this was a new session, also store the user message now
|
|
518
539
|
if (!sessionId) {
|
|
519
|
-
await chatStore.addMessage(resolvedSessionId, { role: 'user', content: prompt })
|
|
540
|
+
await chatStore.addMessage(resolvedSessionId, { role: 'user', content: originalMessage || prompt }, undefined, workspaceId)
|
|
520
541
|
}
|
|
521
542
|
// Store assistant response with turn count
|
|
522
543
|
if (fullResponse) {
|
|
523
|
-
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount)
|
|
544
|
+
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount, workspaceId)
|
|
524
545
|
}
|
|
525
546
|
}
|
|
526
547
|
|
|
@@ -533,7 +554,7 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
533
554
|
}
|
|
534
555
|
|
|
535
556
|
// Load current turn count from session for the done event
|
|
536
|
-
const session = await chatStore.load()
|
|
557
|
+
const session = await chatStore.load(workspaceId)
|
|
537
558
|
const totalTurnCount = session?.turn_count || turnCount
|
|
538
559
|
|
|
539
560
|
const doneEvent = JSON.stringify({
|
package/package.json
CHANGED
package/rules/core.md
CHANGED
|
@@ -40,6 +40,30 @@ Minion
|
|
|
40
40
|
|
|
41
41
|
API の詳細仕様は `~/.minion/docs/api-reference.md` の「Email」セクションを参照。
|
|
42
42
|
|
|
43
|
+
## Terminal Session Management
|
|
44
|
+
|
|
45
|
+
ターミナルセッション(`/api/terminal/*`)を使用する際は以下のルールに従うこと。
|
|
46
|
+
|
|
47
|
+
### セッション再利用の原則
|
|
48
|
+
|
|
49
|
+
- **新しいセッションを作る前に、既存セッションを確認する。**
|
|
50
|
+
```bash
|
|
51
|
+
curl -H "Authorization: Bearer $API_TOKEN" http://localhost:8080/api/terminal/sessions
|
|
52
|
+
```
|
|
53
|
+
- 目的に合う既存セッション(未完了で再利用可能なもの)があれば、そのセッションに `send` でコマンドを送る。
|
|
54
|
+
- 新規作成が必要な場合は、**目的を示す固定名**を付ける(例: `dev`, `build`, `test`)。タイムスタンプ名(`session-1234567890`)は避ける。
|
|
55
|
+
|
|
56
|
+
### クリーンアップ
|
|
57
|
+
|
|
58
|
+
- 作業が完了したセッションは `POST /api/terminal/kill` で終了する。
|
|
59
|
+
- 一時的なコマンド実行(`command` 引数付きで作成したセッション)は、完了確認後に必ず kill する。
|
|
60
|
+
- セッションを放置しない。使い終わったら片付ける。
|
|
61
|
+
|
|
62
|
+
### セッション数の制限
|
|
63
|
+
|
|
64
|
+
- 同時に保持するセッションは **最大3つ** を目安にする。
|
|
65
|
+
- それ以上必要な場合は、不要なセッションを先に kill してから作成する。
|
|
66
|
+
|
|
43
67
|
## Available Tools
|
|
44
68
|
|
|
45
69
|
### CLI
|
package/rules/windows.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Windows Minion
|
|
2
|
+
|
|
3
|
+
Windows ミニオン固有のルール。
|
|
4
|
+
|
|
5
|
+
## WSL セッション管理
|
|
6
|
+
|
|
7
|
+
Windows ミニオンでは WSL(Windows Subsystem for Linux)内の Docker やリポジトリ操作のために WSL セッションを使用できる。
|
|
8
|
+
|
|
9
|
+
### WSL セッションの使い方
|
|
10
|
+
|
|
11
|
+
#### ターミナルセッション
|
|
12
|
+
|
|
13
|
+
WSL 内でコマンドを実行するには `type: "wsl"` を指定してセッションを作成する:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# WSL セッション作成(固定名を付けること)
|
|
17
|
+
curl -X POST http://localhost:8080/api/terminal/create \
|
|
18
|
+
-H "Authorization: Bearer $API_TOKEN" \
|
|
19
|
+
-H "Content-Type: application/json" \
|
|
20
|
+
-d '{"name": "wsl-dev", "type": "wsl"}'
|
|
21
|
+
|
|
22
|
+
# コマンド送信
|
|
23
|
+
curl -X POST http://localhost:8080/api/terminal/send \
|
|
24
|
+
-H "Authorization: Bearer $API_TOKEN" \
|
|
25
|
+
-H "Content-Type: application/json" \
|
|
26
|
+
-d '{"session": "wsl-dev", "input": "docker compose up -d", "enter": true}'
|
|
27
|
+
|
|
28
|
+
# 出力確認
|
|
29
|
+
curl "http://localhost:8080/api/terminal/capture?session=wsl-dev&lines=50" \
|
|
30
|
+
-H "Authorization: Bearer $API_TOKEN"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
#### チャット(WSL モード)
|
|
34
|
+
|
|
35
|
+
チャット API に `wsl_mode: true` を追加すると、Claude Code CLI がユーザーセッションで実行され WSL コマンドを直接使用できる:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
curl -X POST http://localhost:8080/api/chat \
|
|
39
|
+
-H "Authorization: Bearer $API_TOKEN" \
|
|
40
|
+
-H "Content-Type: application/json" \
|
|
41
|
+
-d '{"message": "WSL内でリポジトリをクローンしてDockerを起動して", "wsl_mode": true}'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### セッション管理ルール
|
|
45
|
+
|
|
46
|
+
#### 再利用を徹底する
|
|
47
|
+
|
|
48
|
+
- **WSL セッションの新規作成前に、必ず既存セッションを確認する。**
|
|
49
|
+
```bash
|
|
50
|
+
curl -H "Authorization: Bearer $API_TOKEN" http://localhost:8080/api/terminal/sessions
|
|
51
|
+
```
|
|
52
|
+
レスポンスの `type: "wsl"` かつ `completed: false` のセッションがあれば再利用する。
|
|
53
|
+
- **固定名を使う。** `wsl-dev`, `wsl-build`, `wsl-docker` など目的を示す名前を付ける。
|
|
54
|
+
名前を省略すると `wsl-session-{timestamp}` が自動生成され、再利用できなくなる。
|
|
55
|
+
- **1つの WSL セッションで複数コマンドを順次実行する。** コマンドごとに新しいセッションを作らない。
|
|
56
|
+
|
|
57
|
+
#### クリーンアップ
|
|
58
|
+
|
|
59
|
+
- WSL 作業が完了したら `POST /api/terminal/kill` でセッションを終了する。
|
|
60
|
+
- WSL セッションは最大 **5つ** まで。上限に達すると新規作成が拒否される。
|
|
61
|
+
- 完了済み(`completed: true`)のセッションは自動的にクリーンアップされる。
|
|
62
|
+
|
|
63
|
+
#### WSL セッションサーバーの確認
|
|
64
|
+
|
|
65
|
+
WSL セッションが使えない場合は、まずサーバーの稼働状態を確認する:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
curl http://localhost:8080/api/terminal/wsl/status \
|
|
69
|
+
-H "Authorization: Bearer $API_TOKEN"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`running: false` の場合、ターゲットユーザーがログインしていない可能性がある。
|
package/win/routes/chat.js
CHANGED
|
@@ -116,17 +116,18 @@ async function chatRoutes(fastify) {
|
|
|
116
116
|
return { success: false, error: 'Unauthorized' }
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const { message, session_id, context, wsl_mode } = request.body || {}
|
|
119
|
+
const { message, session_id, context, wsl_mode, workspace_id } = request.body || {}
|
|
120
120
|
if (!message || typeof message !== 'string') {
|
|
121
121
|
reply.code(400)
|
|
122
122
|
return { success: false, error: 'message is required' }
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
const workspaceId = workspace_id || null
|
|
125
126
|
const prompt = await buildContextPrefix(message, context, session_id)
|
|
126
127
|
const currentSessionId = session_id || null
|
|
127
128
|
|
|
128
129
|
if (currentSessionId) {
|
|
129
|
-
await chatStore.addMessage(currentSessionId, { role: 'user', content: message })
|
|
130
|
+
await chatStore.addMessage(currentSessionId, { role: 'user', content: message }, undefined, workspaceId)
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
reply.hijack()
|
|
@@ -144,7 +145,7 @@ async function chatRoutes(fastify) {
|
|
|
144
145
|
await proxyWslChat(reply.raw, prompt, currentSessionId)
|
|
145
146
|
wslModeActive = false
|
|
146
147
|
} else {
|
|
147
|
-
await streamLlmResponse(reply.raw, prompt, currentSessionId)
|
|
148
|
+
await streamLlmResponse(reply.raw, prompt, currentSessionId, workspaceId, message)
|
|
148
149
|
}
|
|
149
150
|
} catch (err) {
|
|
150
151
|
wslModeActive = false
|
|
@@ -160,12 +161,14 @@ async function chatRoutes(fastify) {
|
|
|
160
161
|
reply.code(401)
|
|
161
162
|
return { success: false, error: 'Unauthorized' }
|
|
162
163
|
}
|
|
163
|
-
const
|
|
164
|
+
const workspaceId = request.query?.workspace_id || null
|
|
165
|
+
const session = chatStore.load(workspaceId)
|
|
164
166
|
if (!session) return { success: true, session: null }
|
|
165
167
|
return {
|
|
166
168
|
success: true,
|
|
167
169
|
session: {
|
|
168
170
|
session_id: session.session_id,
|
|
171
|
+
workspace_id: session.workspace_id,
|
|
169
172
|
messages: session.messages,
|
|
170
173
|
turn_count: session.turn_count,
|
|
171
174
|
created_at: session.created_at,
|
|
@@ -174,7 +177,7 @@ async function chatRoutes(fastify) {
|
|
|
174
177
|
}
|
|
175
178
|
})
|
|
176
179
|
|
|
177
|
-
// GET /api/chat/sessions - List
|
|
180
|
+
// GET /api/chat/sessions - List sessions, optionally filtered by workspace
|
|
178
181
|
fastify.get('/api/chat/sessions', async (request, reply) => {
|
|
179
182
|
if (!verifyToken(request)) {
|
|
180
183
|
reply.code(401)
|
|
@@ -182,7 +185,8 @@ async function chatRoutes(fastify) {
|
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
const limit = parseInt(request.query?.limit) || 50
|
|
185
|
-
const
|
|
188
|
+
const workspaceId = request.query?.workspace_id || undefined
|
|
189
|
+
const sessions = chatStore.listSessions(limit, workspaceId)
|
|
186
190
|
return { success: true, sessions }
|
|
187
191
|
})
|
|
188
192
|
|
|
@@ -207,7 +211,8 @@ async function chatRoutes(fastify) {
|
|
|
207
211
|
reply.code(401)
|
|
208
212
|
return { success: false, error: 'Unauthorized' }
|
|
209
213
|
}
|
|
210
|
-
|
|
214
|
+
const workspaceId = request.body?.workspace_id || null
|
|
215
|
+
await chatStore.clear(workspaceId)
|
|
211
216
|
return { success: true }
|
|
212
217
|
})
|
|
213
218
|
|
|
@@ -239,40 +244,57 @@ async function chatRoutes(fastify) {
|
|
|
239
244
|
return { success: true }
|
|
240
245
|
})
|
|
241
246
|
|
|
242
|
-
// POST /api/chat/reset -
|
|
247
|
+
// POST /api/chat/reset - Carry over relevant messages and start fresh session
|
|
243
248
|
fastify.post('/api/chat/reset', async (request, reply) => {
|
|
244
249
|
if (!verifyToken(request)) {
|
|
245
250
|
reply.code(401)
|
|
246
251
|
return { success: false, error: 'Unauthorized' }
|
|
247
252
|
}
|
|
248
253
|
|
|
249
|
-
const
|
|
254
|
+
const workspaceId = request.body?.workspace_id || null
|
|
255
|
+
const session = await chatStore.load(workspaceId)
|
|
250
256
|
if (!session || session.messages.length === 0) {
|
|
251
|
-
await chatStore.clear()
|
|
252
|
-
return { success: true,
|
|
257
|
+
await chatStore.clear(workspaceId)
|
|
258
|
+
return { success: true, carry_over: null }
|
|
253
259
|
}
|
|
254
260
|
|
|
261
|
+
// Take recent messages for topic analysis
|
|
255
262
|
const recentMessages = session.messages.slice(-20)
|
|
256
|
-
|
|
257
|
-
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 500)}`)
|
|
258
|
-
.join('\n\n')
|
|
259
|
-
|
|
260
|
-
const summarizePrompt = `以下の会話を要約してください。重要なコンテキスト、決定事項、進行中のタスクを含めてください。200文字以内で。\n\n${conversationText}`
|
|
263
|
+
let carryOverMessages = recentMessages
|
|
261
264
|
|
|
262
|
-
|
|
265
|
+
// Ask LLM to find where the current topic begins
|
|
263
266
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
console.error('[Chat] summarization failed:', err.message)
|
|
267
|
-
const fallback = recentMessages.slice(-4)
|
|
268
|
-
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 200)}`)
|
|
267
|
+
const indexed = recentMessages
|
|
268
|
+
.map((m, i) => `[${i}] ${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 300)}`)
|
|
269
269
|
.join('\n')
|
|
270
|
-
|
|
270
|
+
|
|
271
|
+
const analyzePrompt = `以下の会話履歴を分析し、現在進行中のトピックが始まったインデックス番号を特定してください。
|
|
272
|
+
会話の流れが明確に変わったポイント(新しい質問・別の作業への切り替え等)がある場合、最後の切り替え以降のインデックスを返してください。
|
|
273
|
+
全体が同じトピックの場合は 0 を返してください。
|
|
274
|
+
|
|
275
|
+
数値のみを返してください(例: 5)。
|
|
276
|
+
|
|
277
|
+
${indexed}`
|
|
278
|
+
|
|
279
|
+
const result = await runQuickLlmCall(analyzePrompt)
|
|
280
|
+
const breakIndex = parseInt(result.trim(), 10)
|
|
281
|
+
if (!isNaN(breakIndex) && breakIndex >= 0 && breakIndex < recentMessages.length) {
|
|
282
|
+
carryOverMessages = recentMessages.slice(breakIndex)
|
|
283
|
+
console.log(`[Chat] topic break at index ${breakIndex}, carrying over ${carryOverMessages.length}/${recentMessages.length} messages`)
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error('[Chat] topic analysis failed, carrying over last 10:', err.message)
|
|
287
|
+
carryOverMessages = recentMessages.slice(-10)
|
|
271
288
|
}
|
|
272
289
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
290
|
+
// Format carry-over as conversation text
|
|
291
|
+
const carryOver = carryOverMessages
|
|
292
|
+
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content}`)
|
|
293
|
+
.join('\n\n')
|
|
294
|
+
|
|
295
|
+
await chatStore.clear(workspaceId)
|
|
296
|
+
console.log(`[Chat] session reset, carrying over ${carryOverMessages.length} messages (${carryOver.length} chars)`)
|
|
297
|
+
return { success: true, carry_over: carryOver }
|
|
276
298
|
})
|
|
277
299
|
|
|
278
300
|
// POST /api/chat/end-of-day - Generate daily log + extract memories
|
|
@@ -394,7 +416,7 @@ function getLlmBinary() {
|
|
|
394
416
|
* Tracks block types to correctly forward tool_use vs text events
|
|
395
417
|
* and counts turns for session management.
|
|
396
418
|
*/
|
|
397
|
-
function streamLlmResponse(res, prompt, sessionId) {
|
|
419
|
+
function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage) {
|
|
398
420
|
return new Promise((resolve, reject) => {
|
|
399
421
|
const binaryName = getLlmBinary()
|
|
400
422
|
if (!binaryName) {
|
|
@@ -542,10 +564,10 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
542
564
|
activeChatChild = null
|
|
543
565
|
if (resolvedSessionId) {
|
|
544
566
|
if (!sessionId) {
|
|
545
|
-
await chatStore.addMessage(resolvedSessionId, { role: 'user', content: prompt })
|
|
567
|
+
await chatStore.addMessage(resolvedSessionId, { role: 'user', content: originalMessage || prompt }, undefined, workspaceId)
|
|
546
568
|
}
|
|
547
569
|
if (fullResponse) {
|
|
548
|
-
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount)
|
|
570
|
+
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount, workspaceId)
|
|
549
571
|
}
|
|
550
572
|
}
|
|
551
573
|
if (code !== 0 && !fullResponse) {
|
|
@@ -553,7 +575,7 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
553
575
|
res.write(`data: ${JSON.stringify({ type: 'error', error: errorMsg })}\n\n`)
|
|
554
576
|
}
|
|
555
577
|
|
|
556
|
-
const session = await chatStore.load()
|
|
578
|
+
const session = await chatStore.load(workspaceId)
|
|
557
579
|
const totalTurnCount = session?.turn_count || turnCount
|
|
558
580
|
|
|
559
581
|
res.write(`data: ${JSON.stringify({ type: 'done', session_id: resolvedSessionId, turn_count: totalTurnCount })}\n\n`)
|
package/win/routes/terminal.js
CHANGED
|
@@ -316,13 +316,26 @@ async function terminalRoutes(fastify) {
|
|
|
316
316
|
if (type === 'wsl') {
|
|
317
317
|
const sessionName = name || `wsl-session-${Date.now()}`
|
|
318
318
|
const wslName = sessionName.startsWith('wsl-') ? sessionName : `wsl-${sessionName}`
|
|
319
|
+
|
|
320
|
+
// get_or_create: return existing session if it's still alive
|
|
321
|
+
if (request.body.get_or_create) {
|
|
322
|
+
const sessions = await proxyToWsl('GET', '/api/wsl/sessions')
|
|
323
|
+
if (sessions && sessions.success && sessions.sessions) {
|
|
324
|
+
const existing = sessions.sessions.find(s => s.name === wslName && !s.completed)
|
|
325
|
+
if (existing) {
|
|
326
|
+
console.log(`[Terminal] Reusing existing WSL session '${wslName}'`)
|
|
327
|
+
return { success: true, session: wslName, message: `Reusing existing WSL session '${wslName}'`, reused: true }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
319
332
|
console.log(`[Terminal] Creating WSL session '${wslName}' — proxying to WSL server`)
|
|
320
333
|
const result = await proxyToWsl('POST', '/api/wsl/create', { name: wslName, command })
|
|
321
334
|
if (!result) {
|
|
322
335
|
reply.code(503)
|
|
323
336
|
return { success: false, error: 'WSL session server is not running. The target user must be logged in for WSL sessions.' }
|
|
324
337
|
}
|
|
325
|
-
reply.code(result.success ? 200 : 500)
|
|
338
|
+
reply.code(result.success ? 200 : (result.error?.includes('limit') ? 429 : 500))
|
|
326
339
|
return result
|
|
327
340
|
}
|
|
328
341
|
|
package/win/server.js
CHANGED
|
@@ -144,10 +144,12 @@ function syncBundledRules() {
|
|
|
144
144
|
try {
|
|
145
145
|
if (!fs.existsSync(bundledRulesDir)) return
|
|
146
146
|
fs.mkdirSync(targetRulesDir, { recursive: true })
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
fs.
|
|
150
|
-
|
|
147
|
+
for (const ruleFile of ['core.md', 'windows.md']) {
|
|
148
|
+
const src = path.join(bundledRulesDir, ruleFile)
|
|
149
|
+
if (fs.existsSync(src)) {
|
|
150
|
+
fs.copyFileSync(src, path.join(targetRulesDir, ruleFile))
|
|
151
|
+
console.log(`[Rules] Synced: ${ruleFile}`)
|
|
152
|
+
}
|
|
151
153
|
}
|
|
152
154
|
for (const legacy of ['minion.md', 'role-pm.md', 'role-engineer.md']) {
|
|
153
155
|
const legacyPath = path.join(targetRulesDir, legacy)
|
|
@@ -29,6 +29,9 @@ const DATA_DIR = path.join(HOME_DIR, '.minion')
|
|
|
29
29
|
const TOKEN_FILE = path.join(DATA_DIR, '.wsl-session-token')
|
|
30
30
|
const PID_FILE = path.join(DATA_DIR, '.wsl-session.pid')
|
|
31
31
|
|
|
32
|
+
const MAX_WSL_SESSIONS = 5
|
|
33
|
+
const COMPLETED_SESSION_TTL_MS = 5 * 60 * 1000 // 5 minutes
|
|
34
|
+
|
|
32
35
|
let AUTH_TOKEN = ''
|
|
33
36
|
try {
|
|
34
37
|
AUTH_TOKEN = fs.readFileSync(TOKEN_FILE, 'utf-8').trim()
|
|
@@ -109,6 +112,7 @@ function createWslSession(sessionName, command) {
|
|
|
109
112
|
ptyProcess.onExit(({ exitCode }) => {
|
|
110
113
|
session.completed = true
|
|
111
114
|
session.exitCode = exitCode
|
|
115
|
+
session.completedAt = Date.now()
|
|
112
116
|
for (const ws of session.wsClients) {
|
|
113
117
|
try { ws.close() } catch {}
|
|
114
118
|
}
|
|
@@ -119,6 +123,30 @@ function createWslSession(sessionName, command) {
|
|
|
119
123
|
return session
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Remove completed sessions that have been idle for COMPLETED_SESSION_TTL_MS.
|
|
128
|
+
*/
|
|
129
|
+
function reapCompletedSessions() {
|
|
130
|
+
const now = Date.now()
|
|
131
|
+
for (const [name, session] of activeSessions) {
|
|
132
|
+
if (session.completed && session.completedAt && now - session.completedAt > COMPLETED_SESSION_TTL_MS) {
|
|
133
|
+
activeSessions.delete(name)
|
|
134
|
+
console.log(`[WSL] Reaped completed session '${name}'`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Count active (non-completed) sessions.
|
|
141
|
+
*/
|
|
142
|
+
function activeSessionCount() {
|
|
143
|
+
let count = 0
|
|
144
|
+
for (const [, session] of activeSessions) {
|
|
145
|
+
if (!session.completed) count++
|
|
146
|
+
}
|
|
147
|
+
return count
|
|
148
|
+
}
|
|
149
|
+
|
|
122
150
|
const specialKeyMap = {
|
|
123
151
|
'Enter': '\r', 'Escape': '\x1b', 'Tab': '\t',
|
|
124
152
|
'C-c': '\x03', 'C-d': '\x04', 'C-z': '\x1a',
|
|
@@ -249,10 +277,24 @@ async function startServer() {
|
|
|
249
277
|
if (!/^[\w-]+$/.test(sessionName)) {
|
|
250
278
|
reply.code(400); return { success: false, error: 'Invalid session name' }
|
|
251
279
|
}
|
|
280
|
+
|
|
281
|
+
// Reap completed sessions before checking limits
|
|
282
|
+
reapCompletedSessions()
|
|
283
|
+
|
|
252
284
|
if (activeSessions.has(sessionName)) {
|
|
253
285
|
reply.code(409); return { success: false, error: `Session '${sessionName}' already exists` }
|
|
254
286
|
}
|
|
255
287
|
|
|
288
|
+
if (activeSessionCount() >= MAX_WSL_SESSIONS) {
|
|
289
|
+
reply.code(429)
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
error: `WSL session limit reached (max ${MAX_WSL_SESSIONS}). Kill unused sessions before creating new ones.`,
|
|
293
|
+
active_sessions: activeSessionCount(),
|
|
294
|
+
max_sessions: MAX_WSL_SESSIONS,
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
256
298
|
try {
|
|
257
299
|
createWslSession(sessionName, command)
|
|
258
300
|
return { success: true, session: sessionName, message: `WSL session '${sessionName}' created` }
|
|
@@ -373,6 +415,9 @@ async function startServer() {
|
|
|
373
415
|
fs.writeFileSync(PID_FILE, String(process.pid))
|
|
374
416
|
} catch {}
|
|
375
417
|
|
|
418
|
+
// Periodically reap completed sessions
|
|
419
|
+
setInterval(reapCompletedSessions, 60 * 1000)
|
|
420
|
+
|
|
376
421
|
return fastify
|
|
377
422
|
}
|
|
378
423
|
|