nx-ce 0.1.6 → 0.1.8
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/README.md +113 -67
- package/package.json +1 -1
- package/src/serve.js +69 -17
package/README.md
CHANGED
|
@@ -41,12 +41,13 @@ nx-ce query "用中文回答:1+1=?" --model claude-haiku-4-5
|
|
|
41
41
|
# Start WebSocket server (persistent, multi-session)
|
|
42
42
|
nx-ce serve --port 3100
|
|
43
43
|
|
|
44
|
-
# In another terminal,
|
|
44
|
+
# In another terminal, run tests
|
|
45
|
+
node test/serve-test.mjs
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
---
|
|
48
49
|
|
|
49
|
-
## `nx-ce query` — One-
|
|
50
|
+
## `nx-ce query` — One-Shot Cold-Start Query / 一次性冷启动查询
|
|
50
51
|
|
|
51
52
|
```bash
|
|
52
53
|
nx-ce query "解释这段代码" --model claude-sonnet-4-6
|
|
@@ -80,9 +81,9 @@ nx-ce query "Analyze" --skill all
|
|
|
80
81
|
|
|
81
82
|
## `nx-ce serve` — WebSocket Multi-Session Server / WebSocket 多会话服务器
|
|
82
83
|
|
|
83
|
-
**Single process. Multiple concurrent sessions.
|
|
84
|
+
**Single process. Multiple concurrent sessions. Each session has its own cwd.**
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
单例进程。多会话隔离。每个会话可指定自己的工作目录。
|
|
86
87
|
|
|
87
88
|
```bash
|
|
88
89
|
nx-ce serve # default port 3100
|
|
@@ -96,7 +97,7 @@ nx-ce serve --name "main" --port 3100 --cwd "D:/project"
|
|
|
96
97
|
| `--port <port>` | WebSocket port (default `3100`) / 端口 |
|
|
97
98
|
| `--model <id>` | Model override / 模型 ID |
|
|
98
99
|
| `--claude-path <path>` | Path to Claude CLI / CLI 路径 |
|
|
99
|
-
| `--cwd <path>` |
|
|
100
|
+
| `--cwd <path>` | Default working directory / 默认工作目录 |
|
|
100
101
|
| `--env "KEY=val,..."` | Extra env vars / 额外环境变量 |
|
|
101
102
|
|
|
102
103
|
> WebSocket address: `ws://127.0.0.1:3100` (localhost only)
|
|
@@ -105,7 +106,78 @@ nx-ce serve --name "main" --port 3100 --cwd "D:/project"
|
|
|
105
106
|
|
|
106
107
|
```bash
|
|
107
108
|
nx-ce serve --port 3100 # first → OK
|
|
108
|
-
nx-ce serve --port 3100 # second → Port
|
|
109
|
+
nx-ce serve --port 3100 # second → Port already in use — another instance is running
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Multi-Session / 多会话管理
|
|
115
|
+
|
|
116
|
+
### Auto-create on first query / 首次 query 自动创建
|
|
117
|
+
|
|
118
|
+
Session 是**隐式创建**的。第一次发 `query` 时自动创建 SDK 会话,之后同名的 query 续接上下文:
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
// 新会话 "proj-a",工作目录 D:/project-a
|
|
122
|
+
→ { "type": "query", "session": "proj-a", "cwd": "D:/project-a", "prompt": "分析目录结构" }
|
|
123
|
+
|
|
124
|
+
// 新会话 "proj-b",工作目录 D:/project-b(不同的 session 名 = 不同的 agentQuery)
|
|
125
|
+
→ { "type": "query", "session": "proj-b", "cwd": "D:/project-b", "prompt": "分析目录结构" }
|
|
126
|
+
|
|
127
|
+
// 同一 session 名 + 不同 cwd → 也是独立的 agentQuery
|
|
128
|
+
→ { "type": "query", "session": "proj-a", "cwd": "D:/project-a/src", "prompt": "分析 src" }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
内部标识 key 格式为 `{name}:{cwd}`:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
proj-a:D~project-a → SDK 会话 A
|
|
135
|
+
proj-b:D~project-b → SDK 会话 B
|
|
136
|
+
proj-a:D~project-a~src → SDK 会话 C
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Close session / 关闭会话
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
// 关闭精确会话(指定 cwd)
|
|
143
|
+
→ { "type": "closeSession", "session": "proj-a", "cwd": "D:/project-a" }
|
|
144
|
+
|
|
145
|
+
// 关闭该 name 下所有 cwd 变体
|
|
146
|
+
→ { "type": "closeSession", "session": "proj-a" }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Idle auto-reclaim / 空闲自动回收
|
|
150
|
+
|
|
151
|
+
客户端断开连接 5 分钟后,session 自动销毁。**历史会话保留**:
|
|
152
|
+
内存中的 session 销毁,但磁盘状态文件保留并标记为 `lifecycleState: 'stopped'`。
|
|
153
|
+
`listSessions` 会同时返回活跃 session(绿色)和历史 session(灰色虚线,可恢复)。
|
|
154
|
+
|
|
155
|
+
### List sessions / 查看会话
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
→ { "type": "listSessions" }
|
|
159
|
+
← { "type": "session_list", "sessions": [
|
|
160
|
+
{ "name": "proj-a", "cwd": "D:/project-a", "sessionId": "sess_xxx",
|
|
161
|
+
"processing": false, "lifecycleState": "active" },
|
|
162
|
+
{ "name": "proj-b", "cwd": "D:/project-b", "sessionId": "sess_yyy",
|
|
163
|
+
"lifecycleState": "stopped",
|
|
164
|
+
"startedAt": "...", "updatedAt": "..." }
|
|
165
|
+
]}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| lifecycleState | 含义 |
|
|
169
|
+
|----------------|------|
|
|
170
|
+
| `active` | 内存中活跃,可直接发 query |
|
|
171
|
+
| `stopped` | 已关闭但保留在磁盘,发 query 会自动 resume(`options.resume = sessionId`) |
|
|
172
|
+
|
|
173
|
+
### Resume a historical session / 恢复历史会话
|
|
174
|
+
|
|
175
|
+
直接对历史 session 发 `query`,服务端会自动用磁盘上保存的 `sessionId` 续接:
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
→ { "type": "query", "session": "proj-a", "cwd": "D:/project-a", "prompt": "继续上次的话题" }
|
|
179
|
+
// server: readState('proj-a:D~/project-a') → sessionId → options.resume
|
|
180
|
+
// 上下文自动恢复
|
|
109
181
|
```
|
|
110
182
|
|
|
111
183
|
---
|
|
@@ -118,21 +190,22 @@ Server: `ws://127.0.0.1:PORT`. All messages are JSON (no length prefix).
|
|
|
118
190
|
|
|
119
191
|
| type | Fields / 字段 | Description / 说明 |
|
|
120
192
|
|------|---------------|-------------------|
|
|
121
|
-
| `query` | `prompt: string`, `session?: string`, `id?: string` | Submit a query / 发起查询 |
|
|
193
|
+
| `query` | `prompt: string`, `session?: string`, `cwd?: string`, `id?: string` | Submit a query / 发起查询 |
|
|
122
194
|
| `ping` | — | Heartbeat / 心跳 |
|
|
123
|
-
| `getSkills` | `session?: string` | Fetch skills/tools/agents / 拉取元数据 |
|
|
124
|
-
| `getStatus` | `session?: string` | Query session status / 查询状态 |
|
|
125
|
-
| `closeSession` | `session: string` | Close
|
|
195
|
+
| `getSkills` | `session?: string`, `cwd?: string` | Fetch skills/tools/agents / 拉取元数据 |
|
|
196
|
+
| `getStatus` | `session?: string`, `cwd?: string` | Query session status / 查询状态 |
|
|
197
|
+
| `closeSession` | `session: string`, `cwd?: string` | Close session(s) / 关闭会话 |
|
|
126
198
|
| `listSessions` | — | List all active sessions / 列出会话 |
|
|
127
199
|
|
|
128
|
-
`session` defaults to `"default"
|
|
200
|
+
`session` defaults to `"default"`.
|
|
129
201
|
|
|
130
202
|
```json
|
|
131
|
-
→ { "type": "query",
|
|
203
|
+
→ { "type": "query", "session": "proj-a", "cwd": "D:/project-a", "prompt": "分析" }
|
|
132
204
|
→ { "type": "ping" }
|
|
133
|
-
→ { "type": "getSkills",
|
|
134
|
-
→ { "type": "getStatus",
|
|
135
|
-
→ { "type": "closeSession",
|
|
205
|
+
→ { "type": "getSkills", "session": "proj-a" }
|
|
206
|
+
→ { "type": "getStatus", "session": "proj-a", "cwd": "D:/project-a" }
|
|
207
|
+
→ { "type": "closeSession", "session": "proj-a", "cwd": "D:/project-a" }
|
|
208
|
+
→ { "type": "closeSession", "session": "proj-a" }
|
|
136
209
|
→ { "type": "listSessions" }
|
|
137
210
|
```
|
|
138
211
|
|
|
@@ -142,7 +215,7 @@ Server: `ws://127.0.0.1:PORT`. All messages are JSON (no length prefix).
|
|
|
142
215
|
|
|
143
216
|
```json
|
|
144
217
|
← { "type": "connected", "port": 3100, "host": "MY-PC",
|
|
145
|
-
"machineId": "744e51b9
|
|
218
|
+
"machineId": "744e51b9-...", "serverTime": 1780736149028 }
|
|
146
219
|
```
|
|
147
220
|
|
|
148
221
|
**Session init (auto-push on first query per session) / 会话初始化(自动推送):**
|
|
@@ -161,7 +234,7 @@ Server: `ws://127.0.0.1:PORT`. All messages are JSON (no length prefix).
|
|
|
161
234
|
← { "type": "turn_start", "turn": "turn_xxx", "time": ... }
|
|
162
235
|
← { "type": "text", "content": "这是一段回复...", "time": ... }
|
|
163
236
|
← { "type": "thinking", "content": "模型思考过程...", "time": ... }
|
|
164
|
-
← { "type": "tool_use", "name": "readFile", "input": {...}
|
|
237
|
+
← { "type": "tool_use", "name": "readFile", "input": {...} }
|
|
165
238
|
← { "type": "done", "sessionId": "sess_xxx", "time": ... }
|
|
166
239
|
```
|
|
167
240
|
|
|
@@ -170,19 +243,19 @@ Server: `ws://127.0.0.1:PORT`. All messages are JSON (no length prefix).
|
|
|
170
243
|
```json
|
|
171
244
|
← { "type": "pong", "sessionId": "sess_xxx", "serverTime": ... }
|
|
172
245
|
← { "type": "skills", "skills": [...], "tools": [...], ... }
|
|
173
|
-
← { "type": "status", "session": "
|
|
174
|
-
← { "type": "session_list", "sessions": [{ "name": "
|
|
175
|
-
← { "type": "session_closed","session": "
|
|
246
|
+
← { "type": "status", "session": "proj-a", "cwd": "D:/project-a", "sessionId": "...", "isActive": true }
|
|
247
|
+
← { "type": "session_list", "sessions": [{ "name":"proj-a", "cwd":"D:/project-a", ... }, ...] }
|
|
248
|
+
← { "type": "session_closed","session": "proj-a", "cwd": "D:/project-a" }
|
|
176
249
|
← { "type": "error", "content": "error message" }
|
|
177
250
|
```
|
|
178
251
|
|
|
179
252
|
### Full exchange example / 完整示例
|
|
180
253
|
|
|
181
254
|
```
|
|
182
|
-
→ { "type":"query", "session":"
|
|
255
|
+
→ { "type":"query", "session":"proj-a", "cwd":"D:/project-a", "prompt":"Hello" }
|
|
183
256
|
← { "type":"turn_start", "turn":"turn_xxx", "time":... }
|
|
184
|
-
← { "type":"text",
|
|
185
|
-
← { "type":"done",
|
|
257
|
+
← { "type":"text", "content":"Hello! How can I help?" }
|
|
258
|
+
← { "type":"done", "sessionId":"sess_abc", "time":... }
|
|
186
259
|
|
|
187
260
|
→ { "type":"ping" }
|
|
188
261
|
← { "type":"pong", "sessionId":"sess_abc", "serverTime":... }
|
|
@@ -190,51 +263,41 @@ Server: `ws://127.0.0.1:PORT`. All messages are JSON (no length prefix).
|
|
|
190
263
|
|
|
191
264
|
---
|
|
192
265
|
|
|
193
|
-
##
|
|
266
|
+
## Architecture / 架构
|
|
194
267
|
|
|
195
268
|
```
|
|
196
269
|
nx-ce serve (single Node.js process)
|
|
197
|
-
|
|
270
|
+
┌──────────────────────────────────────────────────────────┐
|
|
198
271
|
│ WebSocket Server (127.0.0.1:3100) │
|
|
199
272
|
│ │
|
|
200
273
|
│ SessionManager │
|
|
201
274
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
202
|
-
│ │ "
|
|
203
|
-
│ │ "
|
|
204
|
-
│ │ "
|
|
275
|
+
│ │ "proj-a:D~/project-a" → agentQuery(cwd: project-a) │ │
|
|
276
|
+
│ │ "proj-b:D~/project-b" → agentQuery(cwd: project-b) │ │
|
|
277
|
+
│ │ "proj-a:D~/other" → agentQuery(cwd: other) │ │
|
|
205
278
|
│ └──────────────────────┬──────────────────────────────┘ │
|
|
206
279
|
│ spawn each | (SDK manages CLI processes) │
|
|
207
|
-
│
|
|
280
|
+
│ Claude CLI ──────────┴── Claude CLI ──── Claude CLI │
|
|
208
281
|
└───────────────────────────────────────────────────────────┘
|
|
209
282
|
```
|
|
210
283
|
|
|
284
|
+
### Session identity / 会话标识
|
|
285
|
+
|
|
286
|
+
Session key = `{name}:{cwd}`. Same name + different cwd = different SDK session.
|
|
287
|
+
Each session has its own `agentQuery()`, `MessageChannel`, `MonotonicClock`, and state file.
|
|
288
|
+
|
|
211
289
|
### Concurrency guarantees / 竞态保护
|
|
212
290
|
|
|
213
291
|
| Race / 竞态 | Solution / 方案 |
|
|
214
292
|
|-------------|----------------|
|
|
215
293
|
| Concurrent session creation | `_pendingCreates` Map deduplicates in-flight creation promises |
|
|
216
294
|
| SDK response routing | Each session has independent `for await` loop, writes only to `session.client` |
|
|
217
|
-
| State file overwrite | Per-session files (`{name}.json`)
|
|
218
|
-
| Message ordering | Per-session `MonotonicClock` ensures strict
|
|
295
|
+
| State file overwrite | Per-session files (`{name~cwd}.json`) |
|
|
296
|
+
| Message ordering | Per-session `MonotonicClock` ensures strict ordering |
|
|
219
297
|
| Client disconnect cleanup | Null client ref + clear queue + 5-min idle timeout auto-destroy |
|
|
220
298
|
|
|
221
299
|
---
|
|
222
300
|
|
|
223
|
-
## `nx-ce status` — Instance Status / 查看实例状态
|
|
224
|
-
|
|
225
|
-
```bash
|
|
226
|
-
nx-ce status # List all instances
|
|
227
|
-
nx-ce status --name chat-1 # Show specific instance
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
```json
|
|
231
|
-
{ "name": "chat-1", "pid": 12345, "lifecycleState": "running",
|
|
232
|
-
"sessionId": "sess_abc", "model": "claude-sonnet-4-6",
|
|
233
|
-
"port": 3100, "host": "MY-PC" }
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
301
|
## `nx-ce skills` — List Available Skills / 列出可用 Skill
|
|
239
302
|
|
|
240
303
|
```bash
|
|
@@ -252,18 +315,16 @@ nx-ce skills --cwd "D:/project"
|
|
|
252
315
|
|
|
253
316
|
## State Persistence / 状态持久化
|
|
254
317
|
|
|
255
|
-
State files at `~/.nx-ce/instances/{
|
|
318
|
+
State files at `~/.nx-ce/instances/{key}.json`. Key format: `{name}~{cwd}`.
|
|
256
319
|
|
|
257
320
|
```json
|
|
258
321
|
{
|
|
259
|
-
"name": "
|
|
260
|
-
"pid": 12345,
|
|
261
|
-
"startedAt": "2026-06-06T10:30:00.000Z",
|
|
262
|
-
"updatedAt": "2026-06-06T11:00:00.000Z",
|
|
322
|
+
"name": "proj-a:D~/project-a",
|
|
263
323
|
"sessionId": "sess_abc123",
|
|
264
324
|
"model": "claude-sonnet-4-6",
|
|
325
|
+
"cwd": "D:/project-a",
|
|
265
326
|
"host": "MY-PC",
|
|
266
|
-
"machineId": "
|
|
327
|
+
"machineId": "744e51b9-...",
|
|
267
328
|
"lifecycleState": "running",
|
|
268
329
|
"port": 3100,
|
|
269
330
|
"usage": { "inputTokens": 1500, "outputTokens": 3200, ... }
|
|
@@ -279,21 +340,6 @@ State files at `~/.nx-ce/instances/{name}.json`:
|
|
|
279
340
|
|
|
280
341
|
---
|
|
281
342
|
|
|
282
|
-
## Architecture / 架构
|
|
283
|
-
|
|
284
|
-
```
|
|
285
|
-
Chrome Extension / 浏览器扩展
|
|
286
|
-
↕ WebSocket (ws://127.0.0.1:3100)
|
|
287
|
-
nx-ce serve (Node.js)
|
|
288
|
-
├─ SessionManager → agentQuery()
|
|
289
|
-
│ ↕
|
|
290
|
-
│ Claude CLI
|
|
291
|
-
│
|
|
292
|
-
└─ (Native Host via exec.Command → nx-ce query --resume)
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
343
|
## Development / 开发
|
|
298
344
|
|
|
299
345
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nx-ce",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Claude Engine — SDK adapter layer for native messaging host. Bridges @anthropic-ai/claude-agent-sdk calls over a length-prefixed JSON protocol.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/serve.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { WebSocketServer } from 'ws';
|
|
19
19
|
import { query as agentQuery } from '@anthropic-ai/claude-agent-sdk';
|
|
20
20
|
import { hostname, machine, platform, release } from 'node:os';
|
|
21
|
-
import { readState, writeState, deleteState, LifecycleState, createState } from './session-store.js';
|
|
21
|
+
import { readState, writeState, deleteState, listStates, LifecycleState, createState } from './session-store.js';
|
|
22
22
|
import { generateId, MonotonicClock, getMachineId } from './util.js';
|
|
23
23
|
|
|
24
24
|
/** 默认端口 */
|
|
@@ -365,8 +365,13 @@ class SessionManager {
|
|
|
365
365
|
/**
|
|
366
366
|
* 销毁一个 session。
|
|
367
367
|
* @param {string} key - 内部 key(name:cwd)
|
|
368
|
+
* @param {string} reason - 'shutdown' | 'client request' | 'idle timeout' | 'crash'
|
|
369
|
+
* @param {object} [opts]
|
|
370
|
+
* @param {boolean} [opts.keepHistory=true] - 是否保留磁盘状态(标记为 closed)
|
|
371
|
+
* 仅 'shutdown' 和 'crash' 会删除;其他情况保留为历史记录
|
|
368
372
|
*/
|
|
369
|
-
async destroy(key, reason = 'shutdown') {
|
|
373
|
+
async destroy(key, reason = 'shutdown', opts = {}) {
|
|
374
|
+
const { keepHistory = true } = opts;
|
|
370
375
|
const session = this.sessions.get(key);
|
|
371
376
|
if (!session || session.closed) return;
|
|
372
377
|
session.closed = true;
|
|
@@ -379,26 +384,39 @@ class SessionManager {
|
|
|
379
384
|
|
|
380
385
|
this.sessions.delete(key);
|
|
381
386
|
|
|
382
|
-
if (reason
|
|
387
|
+
if (reason === 'shutdown' || reason === 'crash' || !keepHistory) {
|
|
383
388
|
deleteState(key);
|
|
389
|
+
} else {
|
|
390
|
+
// 标记为 closed,保留历史供 listSessions / resume 使用
|
|
391
|
+
const prev = readState(key);
|
|
392
|
+
if (prev) {
|
|
393
|
+
writeState(key, {
|
|
394
|
+
...prev,
|
|
395
|
+
lifecycleState: LifecycleState.STOPPED,
|
|
396
|
+
closedAt: new Date().toISOString(),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
384
399
|
}
|
|
385
400
|
}
|
|
386
401
|
|
|
387
402
|
/**
|
|
388
403
|
* 按客户端 name 销毁匹配的所有 session(包括不同 cwd)。
|
|
389
404
|
* @param {string} name - 客户端传入的 session 名称
|
|
405
|
+
* @param {object} [opts] - 透传给 destroy()
|
|
390
406
|
*/
|
|
391
|
-
async destroyByName(name) {
|
|
407
|
+
async destroyByName(name, opts = {}) {
|
|
392
408
|
const keys = [...this.sessions.keys()].filter(k => baseName(k) === name);
|
|
393
|
-
await Promise.allSettled(keys.map(k => this.destroy(k, 'client request')));
|
|
409
|
+
await Promise.allSettled(keys.map(k => this.destroy(k, 'client request', opts)));
|
|
394
410
|
}
|
|
395
411
|
|
|
396
412
|
/**
|
|
397
413
|
* 销毁所有 session。
|
|
414
|
+
* @param {string} reason
|
|
415
|
+
* @param {object} [opts]
|
|
398
416
|
*/
|
|
399
|
-
async destroyAll(reason = 'shutdown') {
|
|
417
|
+
async destroyAll(reason = 'shutdown', opts = {}) {
|
|
400
418
|
const keys = [...this.sessions.keys()];
|
|
401
|
-
await Promise.allSettled(keys.map(k => this.destroy(k, reason)));
|
|
419
|
+
await Promise.allSettled(keys.map(k => this.destroy(k, reason, opts)));
|
|
402
420
|
}
|
|
403
421
|
}
|
|
404
422
|
|
|
@@ -532,30 +550,64 @@ export async function startServe(options) {
|
|
|
532
550
|
|
|
533
551
|
case 'closeSession': {
|
|
534
552
|
if (req.cwd) {
|
|
535
|
-
// 精确关闭:name + cwd
|
|
553
|
+
// 精确关闭:name + cwd(保留历史)
|
|
536
554
|
const key = sessionKey(sessionName, req.cwd);
|
|
537
|
-
await sessionManager.destroy(key, 'client request');
|
|
555
|
+
await sessionManager.destroy(key, 'client request', { keepHistory: true });
|
|
538
556
|
ws.send(JSON.stringify({ type: 'session_closed', session: sessionName, cwd: req.cwd }));
|
|
539
557
|
} else {
|
|
540
|
-
// 关闭该 name 下所有 cwd
|
|
541
|
-
await sessionManager.destroyByName(sessionName);
|
|
558
|
+
// 关闭该 name 下所有 cwd 变体(保留历史)
|
|
559
|
+
await sessionManager.destroyByName(sessionName, { keepHistory: true });
|
|
542
560
|
ws.send(JSON.stringify({ type: 'session_closed', session: sessionName }));
|
|
543
561
|
}
|
|
544
562
|
break;
|
|
545
563
|
}
|
|
546
564
|
|
|
547
565
|
case 'listSessions': {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
566
|
+
// 合并:内存中活跃 session + 磁盘上历史 session
|
|
567
|
+
// 活跃优先(同 key 用活跃的元数据覆盖历史的)
|
|
568
|
+
const activeByKey = new Map();
|
|
569
|
+
for (const [key, s] of sessionManager.sessions) {
|
|
570
|
+
if (s.closed) continue;
|
|
571
|
+
activeByKey.set(key, {
|
|
552
572
|
key,
|
|
573
|
+
name: s.name,
|
|
553
574
|
cwd: s.cwd,
|
|
554
575
|
sessionId: s.sessionId,
|
|
576
|
+
model: s.sdkOptions?.model,
|
|
555
577
|
queueLength: s.queue.length,
|
|
556
578
|
processing: s.processing,
|
|
557
|
-
|
|
558
|
-
|
|
579
|
+
lifecycleState: 'active',
|
|
580
|
+
startedAt: s.existingState?.startedAt,
|
|
581
|
+
updatedAt: s.existingState?.updatedAt,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// 磁盘历史(每个 instance 文件对应一个 session)
|
|
586
|
+
// 跳过服务器级 entry(cwd === null 且 name 是 single token)
|
|
587
|
+
const historical = [];
|
|
588
|
+
for (const { name, state } of listStates()) {
|
|
589
|
+
// 服务器级 state 缺少 cwd 字段,跳过
|
|
590
|
+
if (!state || !state.cwd) continue;
|
|
591
|
+
// 已被活跃集合包含的,跳过
|
|
592
|
+
if (activeByKey.has(name)) continue;
|
|
593
|
+
historical.push({
|
|
594
|
+
key: name,
|
|
595
|
+
name: baseName(name),
|
|
596
|
+
cwd: state.cwd,
|
|
597
|
+
sessionId: state.sessionId,
|
|
598
|
+
model: state.model,
|
|
599
|
+
queueLength: 0,
|
|
600
|
+
processing: false,
|
|
601
|
+
lifecycleState: state.lifecycleState || 'closed',
|
|
602
|
+
startedAt: state.startedAt,
|
|
603
|
+
updatedAt: state.updatedAt,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
ws.send(JSON.stringify({
|
|
608
|
+
type: 'session_list',
|
|
609
|
+
sessions: [...activeByKey.values(), ...historical],
|
|
610
|
+
}));
|
|
559
611
|
break;
|
|
560
612
|
}
|
|
561
613
|
|