dsh-session-conductor 1.3.1 → 1.3.3
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
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
| 自动续跑 | 识别非人为中断(崩溃/限流/超时/错误),自动继续原任务;失败后 30s 重试 | 面板逐会话开关(临时设置,重启即默认关) |
|
|
16
16
|
| 会话分组 | 按工作区分组显示;「在分组下新建会话」 | 设置 → 插件 → 会话分组 |
|
|
17
17
|
| 撤回最后一条消息 | 删除最后一条用户消息及整轮回复(先预览 + 二次确认 + .undo-backup 备份) | 面板按钮 |
|
|
18
|
-
| 损坏修复 | seq-gap / EIO 坏块 / 双格式(jsonl+zstd
|
|
18
|
+
| 损坏修复 | 会话日志修复,四类判据:seq-gap / EIO 坏块 / 双格式(jsonl+zstd 并存)/ **zstd 帧级损坏**。最后这类原先修不了(尾部垃圾 + 末帧内容坏 ⇒ 整体放弃),现在支持**截断到最长可解码前缀**:逐帧解码定位安全点、保留原始帧不重编码、备份后原子写回(代价是丢掉不可解码的尾部帧,会如实报告丢了多少)。另:`supportsRepair` 的后端探测原先按 0.1.x 的接口判,0.2.x 已移除该接口 ⇒ 判据恒 false、面板扫描恒 501;现改用文件系统判据,内容修复不可用时**回退帧修复** | 面板「说明」页按钮 + 插件 HTTP `/repair` 系列 |
|
|
19
19
|
| 价值分析 | LLM 按规则给会话分类(已完成/未完成/久未活跃/活跃中) | 面板按钮 |
|
|
20
20
|
| 全文搜索 | 官方 FTS5 优先 + 内置 zstd 扫描兜底 | 面板搜索框 |
|
|
21
21
|
| 压缩模型选择 | 上下文自动压缩用模型独立选择(省成本) | 设置 → 插件 → 压缩模型 |
|
|
@@ -148,7 +148,7 @@ allowBuilds:
|
|
|
148
148
|
|
|
149
149
|
| 端点 | 方法 | 用途 |
|
|
150
150
|
|---|---|---|
|
|
151
|
-
| `/list` | GET | 会话列表(标题/目录/时间/状态,落盘缓存按 revision
|
|
151
|
+
| `/list` | GET | 会话列表(标题/目录/时间/状态,落盘缓存按 revision 增量)。**`?fromIndex=true`**:与工具 `list_sessions` 的 `fromIndex` 同义 —— 直接读落盘索引、**不做全量构建**,秒回精简列表(`{ok, fromIndex, sessions, hint}`,不含 live 状态与归档态)。加 `?job=1` 或请求头 `Accept: application/x-job` 改为注册宿主后台 job 并即刻返回 jobId(构建慢时不阻塞请求);**HTTP 入口没有执行主体**,宿主 job 控制器可能拒绝无主注册(实测回 `no job controller serves this agent`)—— 此时**不再直接 501**,而是回退为落盘索引快照(`async:false` + `fromIndex:true` + `jobUnavailable` 原因),索引也没有才如实 501 |
|
|
152
152
|
| `/archive` / `/restore` | POST | 归档 / 恢复会话 |
|
|
153
153
|
| `/delete` | POST | 删除会话(拒绝运行中) |
|
|
154
154
|
| `/undo` | POST | 撤回最后一条消息 |
|
package/lib/apply.js
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
import { num } from "./shared/util.js";
|
|
31
31
|
import { registerContextUsageTool, createContextUsageInjector, readContextUsage } from "./features/context-usage/index.js";
|
|
32
32
|
import { buildMemoryRecall } from "./features/memory-recall/index.js";
|
|
33
|
-
import { registerSessionMessagingTools } from "./features/session-messaging/index.js";
|
|
33
|
+
import { registerSessionMessagingTools, readListIndex } from "./features/session-messaging/index.js";
|
|
34
34
|
import { registerMemoryRecallTool } from "./features/memory-recall/index.js";
|
|
35
35
|
import { registerGroupRoutes } from "./group.js";
|
|
36
36
|
import { saveTemplate, saveTemplateFromUrl, saveTemplateFromPath, listTemplateDir, removeTemplate, readTemplateSync, readCompletionConventionSync, collectTemplateSlotText, planGateAllows, planEnforceDenyMessage, TEMPLATE_DEFAULTS, TEMPLATE_SLOTS, TEMPLATE_MAX_BYTES, PLAN_ENFORCE_EDIT_TOOLS, PLAN_ENFORCE_BASH_WRITE_RE } from "./template-inject.js";
|
|
@@ -271,6 +271,19 @@ export async function apply(ctx, config = {}) {
|
|
|
271
271
|
try {
|
|
272
272
|
// 非 GET 请求会改动会话列表内容,先失效列表缓存,使面板随后的 refresh 立刻看到最新(而非 5s TTL 内的旧列表)。
|
|
273
273
|
if (req.method !== "GET") invalidateSessionListCache();
|
|
274
|
+
// 索引直读(`?fromIndex=true`):与 `list_sessions` 工具的 `fromIndex` **同义** ——
|
|
275
|
+
// 直接读落盘索引、**不做全量构建**,秒回精简列表(不含 live 状态与归档态)。
|
|
276
|
+
// 原先路由只认 job / stream 两个声明,传 fromIndex 会被**静默忽略** ⇒ 悄悄退化成
|
|
277
|
+
// 226 秒级的全量构建(本机实测一次 41 秒才回),调用方以为拿到了快照、实际被钉在慢路径上。
|
|
278
|
+
if (String(req.url ?? "").includes("fromIndex=true")) {
|
|
279
|
+
const items = await readListIndex(ctx);
|
|
280
|
+
if (!items) {
|
|
281
|
+
send(res, 200, { ok: true, fromIndex: true, sessions: [], hint: "落盘索引为空或尚未建立;去掉 fromIndex 走一次刷新(会重建索引,冷缓存可能很慢)" });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
send(res, 200, { ok: true, fromIndex: true, sessions: items, hint: "来自落盘索引,不含 live 状态与归档态;要实时状态请去掉 fromIndex 走刷新" });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
274
287
|
// job 模式(`?job=1` 或 `Accept: application/x-job`):冷缓存下全量构建很慢
|
|
275
288
|
// (本机实测 201 份日志 / 1.5 GB 时 226 秒),把构建交给宿主后台 job 并**立即**返回 jobId,
|
|
276
289
|
// 调用方改用 job_output / job_list 取结果,前台不再被钉住。
|
|
@@ -294,7 +307,22 @@ export async function apply(ctx, config = {}) {
|
|
|
294
307
|
try {
|
|
295
308
|
send(res, 200, { ok: true, async: true, jobId: jobs.start(spec), hint: "用宿主 job_output / job_list 取结果" });
|
|
296
309
|
} catch (error) {
|
|
297
|
-
|
|
310
|
+
// 后台 job 不可用(HTTP 入口**没有执行主体**,宿主 job 控制器会拒绝无主注册 ——
|
|
311
|
+
// 实测回的是「no job controller serves this agent」):不再直接 501 把调用方堵死;
|
|
312
|
+
// 退一步给**索引快照**(秒回、明确标注来源与「不含 live 状态」),索引也没有才如实 501。
|
|
313
|
+
const items = await readListIndex(ctx).catch(() => null);
|
|
314
|
+
if (items) {
|
|
315
|
+
send(res, 200, {
|
|
316
|
+
ok: true,
|
|
317
|
+
async: false,
|
|
318
|
+
fromIndex: true,
|
|
319
|
+
sessions: items,
|
|
320
|
+
jobUnavailable: String(error?.message ?? error),
|
|
321
|
+
hint: "后台 job 不可用,已回退为落盘索引快照(不含 live 状态);要实时状态请走默认或流式分支",
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
send(res, 501, { ok: false, error: { code: "unsupported", message: `注册后台 job 失败:${String(error?.message ?? error)}(且落盘索引不可用,无回退)` } });
|
|
298
326
|
}
|
|
299
327
|
return;
|
|
300
328
|
}
|
package/lib/core/domain.js
CHANGED
|
@@ -115,30 +115,50 @@ export function pluginDomain(ctx) {
|
|
|
115
115
|
if (!storage) return Promise.reject(new Error("storageDomain 不可用"));
|
|
116
116
|
domainPromise = storage.open(pluginDomainSpec).then((domain) => {
|
|
117
117
|
domainLive = true;
|
|
118
|
-
|
|
119
|
-
// ctx.effect 会抛 cannot create effect on inactive context,且 rejected Promise 被缓存后 list 一直 500。
|
|
120
|
-
try {
|
|
121
|
-
ctx.effect(() => () => {
|
|
122
|
-
// 关闭的同时把缓存与存活标记一起清掉——下一次 apply 才会重新 open。
|
|
123
|
-
// 【原代码】只 return domain.close()(缓存不清),靠 apply 开头无条件 domainPromise = null 兜,
|
|
124
|
-
// 遇到「域还活着就重载」时会在旧的活域上再 open 一次 → storage 报 already open → list 500。
|
|
125
|
-
domainLive = false;
|
|
126
|
-
domainPromise = null;
|
|
127
|
-
return domain.close();
|
|
128
|
-
}, "session-conductor: domain close");
|
|
129
|
-
} catch {
|
|
130
|
-
/* 请求路径打开时不挂 effect;apply() 已预热并挂过 */
|
|
131
|
-
}
|
|
118
|
+
attachDomainClose(ctx, domain);
|
|
132
119
|
return domain;
|
|
133
|
-
}).catch((
|
|
120
|
+
}).catch((error) => {
|
|
121
|
+
// 模块热重载场景:本实例的缓存是空的,但上一份实例打开的域仍被 storageDomain
|
|
122
|
+
// 按名字占用(facility 用 reserved 集合挡重复 open),于是再 open 必报 already-open。
|
|
123
|
+
// 宿主提供了按名字查已开域的口子,这里直接复用那份 handle——
|
|
124
|
+
// 否则重载后列表/模板/开关会一路 500,直到整个实例重启为止。
|
|
125
|
+
const reused = isAlreadyOpenError(error) && typeof storage.get === "function"
|
|
126
|
+
? storage.get(pluginDomainSpec.name)
|
|
127
|
+
: undefined;
|
|
128
|
+
if (reused) {
|
|
129
|
+
domainLive = true;
|
|
130
|
+
attachDomainClose(ctx, reused);
|
|
131
|
+
return reused;
|
|
132
|
+
}
|
|
134
133
|
domainPromise = null;
|
|
135
134
|
domainLive = false;
|
|
136
|
-
throw
|
|
135
|
+
throw error;
|
|
137
136
|
});
|
|
138
137
|
}
|
|
139
138
|
return domainPromise;
|
|
140
139
|
}
|
|
141
140
|
|
|
141
|
+
/** 判定「这个名字已经被占用」的重复打开错误(宿主 DomainError 带 code,保底看文案)。 */
|
|
142
|
+
function isAlreadyOpenError(error) {
|
|
143
|
+
return error?.code === "already-open" || /is already open/.test(String(error?.message ?? ""));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 挂关闭效果:只在 apply 的活跃 fiber 上成功,请求路径(inactive fiber)会抛并被吞掉。 */
|
|
147
|
+
function attachDomainClose(ctx, domain) {
|
|
148
|
+
try {
|
|
149
|
+
ctx.effect(() => () => {
|
|
150
|
+
// 关闭的同时把缓存与存活标记一起清掉——下一次 apply 才会重新 open。
|
|
151
|
+
// 【原代码】只 return domain.close()(缓存不清),靠 apply 开头无条件 domainPromise = null 兜,
|
|
152
|
+
// 遇到「域还活着就重载」时会在旧的活域上再 open 一次 → storage 报 already open → list 500。
|
|
153
|
+
domainLive = false;
|
|
154
|
+
domainPromise = null;
|
|
155
|
+
return domain.close();
|
|
156
|
+
}, "session-conductor: domain close");
|
|
157
|
+
} catch {
|
|
158
|
+
/* 请求路径打开时不挂 effect;apply() 已预热并挂过 */
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
142
162
|
/** 读域里的全局状态对象。 */
|
|
143
163
|
export async function pluginState(ctx) {
|
|
144
164
|
const domain = await pluginDomain(ctx);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// 【设计口径】全部走本体官方 API(sessionController 的 create / selectModel / prompt),
|
|
13
13
|
// 不自己写会话日志 —— 原因见 logic.js 头部注释。
|
|
14
14
|
|
|
15
|
-
export { registerSessionMessagingTools } from "./tools.js";
|
|
15
|
+
export { registerSessionMessagingTools, readListIndex } from "./tools.js";
|
|
16
16
|
export {
|
|
17
17
|
formatSessionList,
|
|
18
18
|
resetMessagingStateForTest,
|
|
@@ -58,7 +58,7 @@ function startHostJob(jobs, spec, owner) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** 从落盘索引直接组装一份精简列表(不重建索引)。索引不可用时返回 null。 */
|
|
61
|
-
async function readListIndex(ctx) {
|
|
61
|
+
export async function readListIndex(ctx) {
|
|
62
62
|
const { loadListDiskCache } = await import("../../sessions/list-cache.js");
|
|
63
63
|
const entries = loadListDiskCache(ctx)?.entries;
|
|
64
64
|
if (!entries || typeof entries !== "object") return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-conductor",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "DSH session management enhancement: archive/restore sessions, batch & conditional delete, AI auto-rename, interrupted-session auto-resume, release idle sessions, workspace grouping, undo last message, repair damaged session logs, value analysis, full-text search, per-member model switching (list_models / set_member_model), and plan/closing template injection (system-prompt + context).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -176,7 +176,7 @@ whenToUse: 需要了解 dsh-session-conductor 某个功能怎么用/参数是什
|
|
|
176
176
|
|
|
177
177
|
| 工具 | 用途 | 关键参数 |
|
|
178
178
|
|---|---|---|
|
|
179
|
-
| `list_sessions` | 列出本机会话(id / 标题 / 工作区 / 状态 / 最后活动 / 是否当前会话);支持 `query` 子串过滤 + `limit`/`offset` 分页(**默认只给 20 条**,回执会给出下一页的 offset
|
|
179
|
+
| `list_sessions` | 列出本机会话(id / 标题 / 工作区 / 状态 / 最后活动 / 是否当前会话);支持 `query` 子串过滤 + `limit`/`offset` 分页(**默认只给 20 条**,回执会给出下一页的 offset)。**冷缓存慢路径不阻塞**:全量构建本机实测 **226 秒**(201 份日志 / 1.5 GB),故「**先等 3 秒,超时转宿主后台 job 并复用同一份在途 Promise**(不重启构建)」⇒ 前台立刻拿 `jobId`,用宿主 `job_output` 取结果;热缓存 ~47 ms 仍直接返回。`fromIndex=true` 直读落盘索引秒回精简列表(id/标题/工作区,**不含 live 状态**)。宿主 job 契约(照 `dsh-git-push` 实测):**owner 必须是有活 agent 的会话 id 字符串**(`exec.agent.id`);带 owner 注册失败 ⇒ 退无主注册再试,两步都失败才同步降级并**把原因带回**(不静默) | `query` / `limit` / `offset` / `fromIndex` |
|
|
180
180
|
| `send_to_session` | 给另一个会话投递消息(不经过人转达) | `sessionId`、`text`、`mode`(queue/steer)、`requestId`、`hops`、`dryRun` |
|
|
181
181
|
| `create_session` | 新建会话并可指定模型,可选带初始消息 | `provider`+`model`、`cwd`/`workspaceId`、`sessionId`、`text`、`dryRun` |
|
|
182
182
|
|