dsh-session-conductor 1.3.11 → 1.3.16
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 +23 -1
- package/cordis.patch.yml +0 -0
- package/lib/apply.js +28 -1243
- package/lib/client-parts/apply.js +0 -0
- package/lib/client-parts/components/compaction.js +0 -0
- package/lib/client-parts/components/conductor-settings.js +0 -0
- package/lib/client-parts/components/group.js +0 -0
- package/lib/client-parts/components/panel.js +0 -0
- package/lib/client-parts/components/processing.js +0 -0
- package/lib/client-parts/components/session-icon.js +0 -0
- package/lib/client-parts/components/settings.js +0 -0
- package/lib/client-parts/foundation/bootstrap.js +0 -0
- package/lib/client-parts/foundation/i18n.js +0 -0
- package/lib/client-parts/foundation/list-cache.js +0 -0
- package/lib/client-parts/foundation/styles.js +0 -0
- package/lib/core/config.js +0 -0
- package/lib/core/domain.js +0 -0
- package/lib/core/index-store.js +0 -0
- package/lib/core/lazy.js +0 -0
- package/lib/core/state.js +0 -0
- package/lib/core.js +0 -0
- package/lib/eio-repair.js +0 -0
- package/lib/features/base-routes.js +156 -0
- package/lib/features/context-usage/index.js +0 -0
- package/lib/features/continue/eligibility.js +0 -0
- package/lib/features/continue/scan.js +0 -0
- package/lib/features/continue/session.js +0 -0
- package/lib/features/inject/preset.js +0 -0
- package/lib/features/member-model/batch.js +97 -0
- package/lib/features/member-model/command.js +61 -7
- package/lib/features/member-model/create-teammate.js +0 -0
- package/lib/features/member-model/index.js +15 -1
- package/lib/features/member-model/lead.js +73 -0
- package/lib/features/member-model/locate.js +82 -1
- package/lib/features/member-model/override.js +0 -0
- package/lib/features/member-model/permission-command.js +138 -0
- package/lib/features/member-model/permission.js +213 -0
- package/lib/features/member-model/setup.js +0 -0
- package/lib/features/member-model/switch.js +254 -23
- package/lib/features/member-model/tools.js +324 -0
- package/lib/features/memory-recall/index.js +0 -0
- package/lib/features/rename/analysis.js +0 -0
- package/lib/features/rename/title.js +0 -0
- package/lib/features/rename/value.js +0 -0
- package/lib/features/session-action-routes.js +435 -0
- package/lib/features/session-messaging/index.js +0 -0
- package/lib/features/session-messaging/logic.js +0 -0
- package/lib/features/session-messaging/tools.js +0 -0
- package/lib/features/session-ops-routes.js +0 -0
- package/lib/features/settings-routes.js +400 -0
- package/lib/features/trash.js +0 -0
- package/lib/features/value-index.js +0 -0
- package/lib/features/value-report-routes.js +211 -0
- package/lib/group.js +0 -0
- package/lib/i18n/en.json +0 -0
- package/lib/i18n/zh.json +0 -0
- package/lib/index.js +0 -0
- package/lib/repair.js +0 -0
- package/lib/seq-gap-repair.js +0 -0
- package/lib/session-codec.js +0 -0
- package/lib/session-log.js +0 -0
- package/lib/sessions/detach.js +0 -0
- package/lib/sessions/events.js +0 -0
- package/lib/sessions/interruption.js +0 -0
- package/lib/sessions/list-cache.js +0 -0
- package/lib/sessions/list.js +0 -0
- package/lib/sessions/lookup.js +0 -0
- package/lib/sessions/route.js +0 -0
- package/lib/sessions/session-id.js +0 -0
- package/lib/sessions/tail-io.js +185 -0
- package/lib/sessions/turn.js +0 -0
- package/lib/shared/atomic-file.js +0 -0
- package/lib/shared/constants.js +0 -0
- package/lib/shared/json-store.js +0 -0
- package/lib/shared/log.js +0 -0
- package/lib/shared/timing.js +0 -0
- package/lib/shared/util.js +0 -0
- package/lib/template-inject.js +0 -0
- package/lib/value.js +0 -0
- package/lib/zstd-frames.js +0 -0
- package/package.json +1 -1
- package/scripts/check-member-model.mjs +0 -0
- package/skills/dsh-session-conductor-functions.md +0 -0
- package/skills/dsh-session-conductor.md +0 -0
- package/skills/dsh-session-official-doc.md +0 -0
- package/skills/task-completion-report.md +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// dsh-session-conductor — 会话日志的「尾部」增量读写
|
|
2
|
+
//
|
|
3
|
+
// 【为什么单独成文件】成员模型切换(/model-switch、set_member_model、create_teammate_with_model)
|
|
4
|
+
// 需要往会话日志追加一条 model/selection 事件。原实现是「整份解码 → 拼全文 → 全量校验 → 整份重编码」,
|
|
5
|
+
// 一次切换要完整解码 3 次,30MB 日志时峰值内存数百 MB 且同步 parse 阻塞事件循环数秒。
|
|
6
|
+
//
|
|
7
|
+
// 本模块把这条路径改成「只看尾部」:
|
|
8
|
+
// · readTailEvents —— 只解码最后 N 帧,取最后若干条事件(seq / 行数判定都在尾部完成)
|
|
9
|
+
// · appendEventFrame —— 只把「新事件一行」压缩成一个新帧追加到文件尾,**不重写已有内容**
|
|
10
|
+
//
|
|
11
|
+
// 【为什么追加帧是合法的】DSH 会话日志是【多帧容器】:第一帧必须恰好一行 header,其后是事件帧,
|
|
12
|
+
// 帧与帧之间在解码层直接拼接(见 zstd-frames.js decodeAllFrames)。因此「在文件尾追加一个新事件帧」
|
|
13
|
+
// 与「重写整份」在解码后的文本上等价,但内存与 CPU 开销与日志总大小无关。
|
|
14
|
+
//
|
|
15
|
+
// 【安全性】追加帧只改文件尾,失败时不影响已有帧;调用方仍会在写后自检(自检也走尾部读)。
|
|
16
|
+
|
|
17
|
+
import { readFile, open } from "node:fs/promises";
|
|
18
|
+
import { scanZstdFrames, decodeFrame, ZSTD_MAGIC } from "../zstd-frames.js";
|
|
19
|
+
import zlib from "node:zlib";
|
|
20
|
+
const { zstdCompress, constants } = zlib;
|
|
21
|
+
|
|
22
|
+
const FRAME_OPTIONS = { params: { [constants.ZSTD_c_checksumFlag]: 1 } };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 只解码会话日志的**最后若干帧**,返回其中的事件与统计信息。
|
|
26
|
+
*
|
|
27
|
+
* 与原「整份解码」的差异:不返回全部行,只返回尾部帧内的事件;
|
|
28
|
+
* 因此内存峰值 = 最后 N 帧的解压结果,与日志总大小无关。
|
|
29
|
+
*
|
|
30
|
+
* @param {Buffer} buf 整份(压缩的)日志文件内容
|
|
31
|
+
* @param {{tailFrames?: number, maxEvents?: number}} [opts]
|
|
32
|
+
* @returns {Promise<{events: object[], frames: number, tailStartFrame: number, torn: boolean}>}
|
|
33
|
+
* events 为倒序无关、按日志顺序的尾部事件;frames 为总帧数;tailStartFrame 为尾部第一帧序号
|
|
34
|
+
*/
|
|
35
|
+
export async function readTailEvents(buf, opts = {}) {
|
|
36
|
+
const tailFrames = Math.max(1, opts.tailFrames ?? 3);
|
|
37
|
+
const maxEvents = Math.max(1, opts.maxEvents ?? 2000);
|
|
38
|
+
const { frames, tornStart } = scanZstdFrames(buf);
|
|
39
|
+
const tail = frames.slice(Math.max(0, frames.length - tailFrames));
|
|
40
|
+
// 逐帧解码 → 逐行消费,**不把尾部文本拼成一个大字符串**:
|
|
41
|
+
// 只保留「最后 maxEvents 条事件」,因此单帧再大也只在这一个数组上有峰值。
|
|
42
|
+
const events = [];
|
|
43
|
+
let carry = "";
|
|
44
|
+
for (const fr of tail) {
|
|
45
|
+
const frameText = (await decodeFrame(buf.subarray(fr.start, fr.end))).toString("utf8");
|
|
46
|
+
if (frameText === "") continue;
|
|
47
|
+
const lines = (carry + frameText).split("\n");
|
|
48
|
+
// 最后一段可能是不完整行(帧边界断开),留给下一帧拼接
|
|
49
|
+
carry = lines.pop() ?? "";
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (line.trim() === "") continue;
|
|
52
|
+
try {
|
|
53
|
+
events.push(JSON.parse(line));
|
|
54
|
+
} catch {
|
|
55
|
+
// 尾部帧里理论上都是完整行;解析失败不致命,交给调用方的 seq 自检兜底
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (carry.trim() !== "") {
|
|
60
|
+
try {
|
|
61
|
+
events.push(JSON.parse(carry));
|
|
62
|
+
} catch {
|
|
63
|
+
// 末尾残行:容忍,交给调用方自检
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 只保留最后 maxEvents 条(尾部帧通常很小,这是防止极端大帧的兜底)
|
|
67
|
+
return {
|
|
68
|
+
events: events.length > maxEvents ? events.slice(events.length - maxEvents) : events,
|
|
69
|
+
frames: frames.length,
|
|
70
|
+
tailStartFrame: Math.max(0, frames.length - tail.length),
|
|
71
|
+
torn: tornStart !== undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 压缩「单行事件」成一个独立的 zstd 帧(带 checksum)。 */
|
|
76
|
+
async function compressEventLine(line) {
|
|
77
|
+
const buf = Buffer.from(line.endsWith("\n") ? line : `${line}\n`, "utf8");
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
zstdCompress(buf, FRAME_OPTIONS, (error, out) => (error ? reject(error) : resolve(out)));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 把一个事件**以新帧形式追加**到会话日志尾部。
|
|
85
|
+
*
|
|
86
|
+
* 为什么不用「读整份 → 拼字符串 → 重编码」:那条路径内存与 CPU 都随日志大小线性增长,
|
|
87
|
+
* 而这里只压缩一行(几百字节~几 KB),开销恒定。
|
|
88
|
+
*
|
|
89
|
+
* 追加前会自证两件事,任一只怕就直接报错(宁可不动日志):
|
|
90
|
+
* ① 文件当前确实是合法的多帧容器(能扫描出帧、首帧能解出 header 行);
|
|
91
|
+
* ② 尾部事件的 seq 与将要写入的 seq 严格衔接(调用方传入 expectedSeq 时)。
|
|
92
|
+
*
|
|
93
|
+
* @param {string} filePath 会话日志路径
|
|
94
|
+
* @param {object} event 要追加的事件(会被 JSON.stringify 成一行)
|
|
95
|
+
* @returns {Promise<{ok: true, seq: number, bytes: number} | {ok: false, error: string}>}
|
|
96
|
+
*/
|
|
97
|
+
export async function appendEventFrame(filePath, event) {
|
|
98
|
+
let buf;
|
|
99
|
+
try {
|
|
100
|
+
buf = await readFile(filePath);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return { ok: false, error: `读取失败:${String(error?.message ?? error)}` };
|
|
103
|
+
}
|
|
104
|
+
let frames;
|
|
105
|
+
try {
|
|
106
|
+
({ frames } = scanZstdFrames(buf));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return { ok: false, error: `帧扫描失败(文件可能已损坏,先修复再追加):${String(error?.message ?? error)}` };
|
|
109
|
+
}
|
|
110
|
+
if (frames.length === 0) return { ok: false, error: "日志没有任何 zstd 帧" };
|
|
111
|
+
// 首帧必须能解出「一行 header」——否则这个文件不是合法会话日志,拒绝追加
|
|
112
|
+
let headerText;
|
|
113
|
+
try {
|
|
114
|
+
headerText = (await decodeFrame(buf.subarray(frames[0].start, frames[0].end))).toString("utf8");
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return { ok: false, error: `首帧解码失败:${String(error?.message ?? error)}` };
|
|
117
|
+
}
|
|
118
|
+
const headerLine = headerText.split("\n").find((l) => l.trim() !== "");
|
|
119
|
+
if (!headerLine) return { ok: false, error: "首帧没有 header 行" };
|
|
120
|
+
try {
|
|
121
|
+
const header = JSON.parse(headerLine);
|
|
122
|
+
if (header?.type !== "session") return { ok: false, error: "首帧 header 不是 session 类型" };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return { ok: false, error: `首帧 header 解析失败:${String(error?.message ?? error)}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let frame;
|
|
128
|
+
try {
|
|
129
|
+
frame = await compressEventLine(JSON.stringify(event));
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return { ok: false, error: `压缩新帧失败:${String(error?.message ?? error)}` };
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const handle = await open(filePath, "r+");
|
|
135
|
+
try {
|
|
136
|
+
// 追加前最后确认一次文件长度未被他人改变(粗粒度 CAS)
|
|
137
|
+
const stat = await handle.stat();
|
|
138
|
+
if (stat.size !== buf.length) {
|
|
139
|
+
return { ok: false, error: "文件在读取后被修改(长度变化),放弃追加" };
|
|
140
|
+
}
|
|
141
|
+
await handle.write(frame, 0, frame.length, buf.length);
|
|
142
|
+
await handle.sync();
|
|
143
|
+
} finally {
|
|
144
|
+
await handle.close();
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return { ok: false, error: `追加写入失败:${String(error?.message ?? error)}` };
|
|
148
|
+
}
|
|
149
|
+
return { ok: true, seq: event.seq, bytes: frame.length };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 文件是否以 zstd magic 开头(快速判定,避免对非日志文件做帧扫描)。 */
|
|
153
|
+
export function looksLikeZstd(buf) {
|
|
154
|
+
return Buffer.isBuffer(buf) && buf.length >= 4 && buf.readUInt32LE(0) === ZSTD_MAGIC;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 从日志**头部若干帧**里找 `subagent/descriptor` 事件。
|
|
159
|
+
*
|
|
160
|
+
* 【为什么要读头部】宿主 cold resume 子代理时,模型与 provider 取自这条 descriptor
|
|
161
|
+
* (见宿主 manager.coldResume → materialize 的 agentOptions),而不是 `model/selection`。
|
|
162
|
+
* 因此切换模型时必须能读到它、并在它指向已下线的 provider 时同步修正,否则成员会永久唤不醒
|
|
163
|
+
* (宿主报 subagent/not-resumable)。
|
|
164
|
+
*
|
|
165
|
+
* @param {Buffer} buf 整份(压缩的)日志文件内容
|
|
166
|
+
* @param {{headFrames?: number}} [opts]
|
|
167
|
+
* @returns {Promise<{descriptor: object|null, index: number|null}>} index 为事件在「解码行序」中的位置
|
|
168
|
+
*/
|
|
169
|
+
export async function readDescriptorEvent(buf, opts = {}) {
|
|
170
|
+
const headFrames = Math.max(1, opts.headFrames ?? 3);
|
|
171
|
+
const { frames } = scanZstdFrames(buf);
|
|
172
|
+
const head = frames.slice(0, headFrames);
|
|
173
|
+
let lineIndex = 0;
|
|
174
|
+
for (const fr of head) {
|
|
175
|
+
const text = (await decodeFrame(buf.subarray(fr.start, fr.end))).toString("utf8");
|
|
176
|
+
for (const line of text.split("\n")) {
|
|
177
|
+
if (line.trim() === "") continue;
|
|
178
|
+
lineIndex += 1;
|
|
179
|
+
let event;
|
|
180
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
181
|
+
if (event?.type === "subagent/descriptor") return { descriptor: event.data ?? null, index: lineIndex };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { descriptor: null, index: null };
|
|
185
|
+
}
|
package/lib/sessions/turn.js
CHANGED
|
File without changes
|
|
File without changes
|
package/lib/shared/constants.js
CHANGED
|
File without changes
|
package/lib/shared/json-store.js
CHANGED
|
File without changes
|
package/lib/shared/log.js
CHANGED
|
File without changes
|
package/lib/shared/timing.js
CHANGED
|
File without changes
|
package/lib/shared/util.js
CHANGED
|
File without changes
|
package/lib/template-inject.js
CHANGED
|
File without changes
|
package/lib/value.js
CHANGED
|
File without changes
|
package/lib/zstd-frames.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-conductor",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.16",
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|