pi-web-ui 0.90.1 → 0.92.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/CHANGELOG.md +81 -1
- package/bin/pi-web-ui.mjs +21 -2
- package/dist/server/agent-service.js +1206 -106
- package/dist/server/claim-files-tool.js +116 -0
- package/dist/server/claim-store.js +301 -0
- package/dist/server/client-state.js +119 -5
- package/dist/server/compaction-markers.js +287 -0
- package/dist/server/conversation-read-tool.js +369 -27
- package/dist/server/conversation-touches.js +357 -0
- package/dist/server/delegate-task.js +12 -1
- package/dist/server/dsh/dsh-agent-service.js +28 -1
- package/dist/server/file-transfer-routes.js +4 -1
- package/dist/server/icon-svg.js +35 -0
- package/dist/server/index.js +152 -34
- package/dist/server/model-admin.js +124 -2
- package/dist/server/model-enrich.js +478 -0
- package/dist/server/plugin-catalog.js +5 -0
- package/dist/server/plugin-facilities.js +42 -11
- package/dist/server/plugin-schedule.js +27 -2
- package/dist/server/plugins.js +112 -18
- package/dist/server/present-files-tool.js +303 -0
- package/dist/server/prompt-composer.js +1 -1
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/read-tool.js +91 -0
- package/dist/server/schedule-agent-tool.js +23 -5
- package/dist/server/scheduler-tasks.js +60 -1
- package/dist/server/scm-commitmsg.js +140 -0
- package/dist/server/scm.js +42 -0
- package/dist/server/serialize.js +21 -1
- package/dist/server/settings-service.js +47 -0
- package/dist/server/skill-tool.js +120 -0
- package/dist/server/soft-cap.js +68 -0
- package/dist/server/subagent-templates.js +6 -0
- package/dist/server/subagents.js +157 -16
- package/dist/server/text-sniff.js +15 -0
- package/dist/server/themes.js +17 -2
- package/dist/server/tool-manager.js +13 -1
- package/package.json +2 -1
- package/plugin-sdk/README.md +1 -0
- package/plugins/catalog.json +20 -0
- package/themes/catppuccin-latte.css +136 -0
- package/themes/catppuccin.css +140 -0
- package/themes/nord.css +130 -0
- package/themes/one-dark.css +131 -0
- package/themes/paper.css +12 -12
- package/themes/solarized-light.css +131 -0
- package/themes/tokyo-night.css +140 -0
- package/web/dist/assets/{TerminalPanel-CkxM4Byl.js → TerminalPanel-RACpk6AX.js} +1 -1
- package/web/dist/assets/es-DIDZppIF.js +2 -0
- package/web/dist/assets/index-BgrnP9Jz.js +364 -0
- package/web/dist/assets/index-Cy8dSTZt.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DJIV4A-t.js +0 -355
- package/web/dist/assets/index-DLdcRBs_.css +0 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction transcript markers + transcript repair (issue #235).
|
|
3
|
+
*
|
|
4
|
+
* 背景:pi-web-ui 在 compaction_start 时往会话 JSONL 尾追加一行自有的 custom
|
|
5
|
+
* 标记(crash-safe 的"正在压缩"记录),compaction_end 时原地改写为完成标记。
|
|
6
|
+
* 0.90.x 用硬编码 id(pi-web-ui-compaction-pending / -done),多次压缩后文件
|
|
7
|
+
* 里出现多个同名 entry;SDK 的 byId 索引是 last-wins,leaf 又是文件最后一行,
|
|
8
|
+
* 若标记恰为末行则新消息的 parentId 记成共享 id,下次 open 回溯 parent 链成环,
|
|
9
|
+
* getBranch 死循环 push 直到 V8 抛 RangeError: Invalid array length,整个会话
|
|
10
|
+
* 打不开("Failed to initialize session")。
|
|
11
|
+
*
|
|
12
|
+
* 本模块:
|
|
13
|
+
* - makeCompactionMarkerId:每次压缩唯一 id,根除新坏文件;
|
|
14
|
+
* - repairSessionTranscript:纯函数,移除残留 pending(其子节点旁路到 pending
|
|
15
|
+
* 的 parent,marker 对 LLM 上下文零贡献)+ 重复 id 改名 + 重复 id 引用改指
|
|
16
|
+
* 最近的前序同名 + 环路截断。只改坏行,其余字节不动;
|
|
17
|
+
* - repairSessionFile:落盘版,改前写 <原名>.bak(已存在则保留最早那份);
|
|
18
|
+
* - looksLikeChainCorruption:识别链损坏报错,供 open 重试分支用。
|
|
19
|
+
*/
|
|
20
|
+
import { randomUUID } from "node:crypto";
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
export const COMPACTION_PENDING_TYPE = "pi-web-ui/compaction-pending";
|
|
23
|
+
export const COMPACTION_DONE_TYPE = "pi-web-ui/compaction-done";
|
|
24
|
+
/** 0.90.x 写死的 id——repair 用来识别存量坏标记。 */
|
|
25
|
+
const LEGACY_PENDING_ID = "pi-web-ui-compaction-pending";
|
|
26
|
+
const LEGACY_DONE_ID = "pi-web-ui-compaction-done";
|
|
27
|
+
/** 每次压缩唯一 id:Date.now 保证先后可辨,random 保证同毫秒不撞。 */
|
|
28
|
+
export function makeCompactionMarkerId(kind) {
|
|
29
|
+
return `pi-web-ui-compaction-${kind}-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
30
|
+
}
|
|
31
|
+
function markerKindOf(entry) {
|
|
32
|
+
if (typeof entry !== "object" || entry === null)
|
|
33
|
+
return null;
|
|
34
|
+
const e = entry;
|
|
35
|
+
if (e.type !== "custom" || typeof e.id !== "string")
|
|
36
|
+
return null;
|
|
37
|
+
if (e.customType === COMPACTION_PENDING_TYPE)
|
|
38
|
+
return "pending";
|
|
39
|
+
if (e.customType === COMPACTION_DONE_TYPE)
|
|
40
|
+
return "done";
|
|
41
|
+
// 兜底:customType 丢失的手工改坏文件,按 id 前缀认(SDK 自生成的 id 不带此前缀)。
|
|
42
|
+
if (e.id === LEGACY_PENDING_ID || e.id.startsWith(`${LEGACY_PENDING_ID}-`))
|
|
43
|
+
return "pending";
|
|
44
|
+
if (e.id === LEGACY_DONE_ID || e.id.startsWith(`${LEGACY_DONE_ID}-`))
|
|
45
|
+
return "done";
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function parentIdOf(entry) {
|
|
49
|
+
const p = entry.parentId;
|
|
50
|
+
return typeof p === "string" ? p : null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 修复会话转录文本。健康文件原样返回(changed=false,text === raw)。
|
|
54
|
+
* 有坏必修:pending 残留→删(子节点旁路);重复 id→首个保留、其余改名,
|
|
55
|
+
* 引用改指最近前序同名;残余环→截断。调用方负责落盘+备份(见 repairSessionFile)。
|
|
56
|
+
*/
|
|
57
|
+
export function repairSessionTranscript(raw) {
|
|
58
|
+
const result = {
|
|
59
|
+
text: raw,
|
|
60
|
+
changed: false,
|
|
61
|
+
removedPending: 0,
|
|
62
|
+
renamedIds: 0,
|
|
63
|
+
rewiredParents: 0,
|
|
64
|
+
cyclesBroken: 0,
|
|
65
|
+
interrupted: false,
|
|
66
|
+
};
|
|
67
|
+
const lines = raw.split("\n");
|
|
68
|
+
const slots = lines.map((line) => {
|
|
69
|
+
if (!line.trim())
|
|
70
|
+
return { raw: line, entry: null, removed: false, dirty: false, finalId: null };
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(line);
|
|
73
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
74
|
+
const id = parsed.id;
|
|
75
|
+
return {
|
|
76
|
+
raw: line,
|
|
77
|
+
entry: parsed,
|
|
78
|
+
removed: false,
|
|
79
|
+
dirty: false,
|
|
80
|
+
finalId: typeof id === "string" ? id : null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// 脏行:SDK 加载时同样跳过,这里原样保留。
|
|
86
|
+
}
|
|
87
|
+
return { raw: line, entry: null, removed: false, dirty: false, finalId: null };
|
|
88
|
+
});
|
|
89
|
+
// 第 1 步:移除残留 pending,记录 id→parent 供子节点旁路。
|
|
90
|
+
const pendingParent = new Map();
|
|
91
|
+
for (const s of slots) {
|
|
92
|
+
if (!s.entry || markerKindOf(s.entry) !== "pending")
|
|
93
|
+
continue;
|
|
94
|
+
const id = typeof s.entry.id === "string" ? s.entry.id : null;
|
|
95
|
+
if (!id)
|
|
96
|
+
continue;
|
|
97
|
+
s.removed = true;
|
|
98
|
+
result.removedPending += 1;
|
|
99
|
+
if (!pendingParent.has(id))
|
|
100
|
+
pendingParent.set(id, parentIdOf(s.entry));
|
|
101
|
+
}
|
|
102
|
+
if (pendingParent.size > 0) {
|
|
103
|
+
const resolveBypass = (id) => {
|
|
104
|
+
// pending 套 pending 理论上不存在,visited 防万一。
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
let cur = id;
|
|
107
|
+
while (cur !== null && pendingParent.has(cur) && !seen.has(cur)) {
|
|
108
|
+
seen.add(cur);
|
|
109
|
+
cur = pendingParent.get(cur) ?? null;
|
|
110
|
+
}
|
|
111
|
+
return cur;
|
|
112
|
+
};
|
|
113
|
+
for (const s of slots) {
|
|
114
|
+
if (s.removed || !s.entry)
|
|
115
|
+
continue;
|
|
116
|
+
const p = parentIdOf(s.entry);
|
|
117
|
+
if (p === null || !pendingParent.has(p))
|
|
118
|
+
continue;
|
|
119
|
+
const next = resolveBypass(p);
|
|
120
|
+
if (next === p)
|
|
121
|
+
continue;
|
|
122
|
+
if (next === null) {
|
|
123
|
+
// pending 本就没有 parent(首行标记)——子节点提升为根。
|
|
124
|
+
s.entry.parentId = null;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
s.entry.parentId = next;
|
|
128
|
+
}
|
|
129
|
+
s.dirty = true;
|
|
130
|
+
result.rewiredParents += 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// 第 2 步:重复 id 改名(首个保留),引用改指"最近的前序同名"。
|
|
134
|
+
// 先算每行的最终 id,再统一改引用,避免先改后认错。
|
|
135
|
+
const live = slots.filter((s) => !s.removed && s.entry && s.finalId !== null);
|
|
136
|
+
const seenIds = new Set();
|
|
137
|
+
const takenIds = new Set();
|
|
138
|
+
for (const s of live)
|
|
139
|
+
takenIds.add(s.finalId);
|
|
140
|
+
const dupSeq = new Map();
|
|
141
|
+
for (const s of live) {
|
|
142
|
+
const id = s.finalId;
|
|
143
|
+
if (!seenIds.has(id)) {
|
|
144
|
+
seenIds.add(id);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
let k = (dupSeq.get(id) ?? 0) + 1;
|
|
148
|
+
let candidate = `${id}--dup-${k}`;
|
|
149
|
+
while (takenIds.has(candidate)) {
|
|
150
|
+
k += 1;
|
|
151
|
+
candidate = `${id}--dup-${k}`;
|
|
152
|
+
}
|
|
153
|
+
dupSeq.set(id, k);
|
|
154
|
+
s.finalId = candidate;
|
|
155
|
+
takenIds.add(candidate);
|
|
156
|
+
result.renamedIds += 1;
|
|
157
|
+
}
|
|
158
|
+
const dupIds = new Set();
|
|
159
|
+
{
|
|
160
|
+
const counts = new Map();
|
|
161
|
+
for (const s of live) {
|
|
162
|
+
const orig = typeof s.entry?.id === "string" ? s.entry.id : null;
|
|
163
|
+
if (orig)
|
|
164
|
+
counts.set(orig, (counts.get(orig) ?? 0) + 1);
|
|
165
|
+
}
|
|
166
|
+
for (const [id, c] of counts)
|
|
167
|
+
if (c > 1)
|
|
168
|
+
dupIds.add(id);
|
|
169
|
+
}
|
|
170
|
+
if (dupIds.size > 0) {
|
|
171
|
+
// origId → 按行序排好的 (行号, 最终id)
|
|
172
|
+
const occ = new Map();
|
|
173
|
+
live.forEach((s, i) => {
|
|
174
|
+
const orig = typeof s.entry?.id === "string" ? s.entry.id : null;
|
|
175
|
+
if (!orig || !dupIds.has(orig))
|
|
176
|
+
return;
|
|
177
|
+
let arr = occ.get(orig);
|
|
178
|
+
if (!arr) {
|
|
179
|
+
arr = [];
|
|
180
|
+
occ.set(orig, arr);
|
|
181
|
+
}
|
|
182
|
+
arr.push({ idx: i, finalId: s.finalId });
|
|
183
|
+
});
|
|
184
|
+
live.forEach((s, i) => {
|
|
185
|
+
if (!s.entry)
|
|
186
|
+
return;
|
|
187
|
+
const p = parentIdOf(s.entry);
|
|
188
|
+
if (p === null || !dupIds.has(p))
|
|
189
|
+
return;
|
|
190
|
+
const arr = occ.get(p);
|
|
191
|
+
let target = null;
|
|
192
|
+
for (const o of arr) {
|
|
193
|
+
if (o.idx < i)
|
|
194
|
+
target = o.finalId;
|
|
195
|
+
else
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
// 引用落在全部同名之前(正常追加不可能,手改文件才有)——保持悬空,
|
|
199
|
+
// SDK 按 orphan 截断,安全降级。
|
|
200
|
+
if (target === null || target === p)
|
|
201
|
+
return;
|
|
202
|
+
s.entry.parentId = target;
|
|
203
|
+
s.dirty = true;
|
|
204
|
+
result.rewiredParents += 1;
|
|
205
|
+
});
|
|
206
|
+
// 应用改名。
|
|
207
|
+
for (const s of live) {
|
|
208
|
+
if (!s.entry || s.finalId === null || s.entry.id === s.finalId)
|
|
209
|
+
continue;
|
|
210
|
+
s.entry.id = s.finalId;
|
|
211
|
+
s.dirty = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// 第 3 步:残余环截断(纵深防御——前两步之后正常已无环)。
|
|
215
|
+
// byId 语义与 SDK 一致:同名取其一(此时已无重复)。
|
|
216
|
+
if (live.length > 0) {
|
|
217
|
+
const byId = new Map();
|
|
218
|
+
for (const s of live)
|
|
219
|
+
if (s.finalId !== null)
|
|
220
|
+
byId.set(s.finalId, s);
|
|
221
|
+
for (const s of live) {
|
|
222
|
+
if (!s.entry)
|
|
223
|
+
continue;
|
|
224
|
+
const seen = new Set([s.finalId]);
|
|
225
|
+
let prev = s;
|
|
226
|
+
let cur = parentIdOf(s.entry);
|
|
227
|
+
while (cur !== null) {
|
|
228
|
+
if (seen.has(cur)) {
|
|
229
|
+
prev.entry.parentId = null;
|
|
230
|
+
prev.dirty = true;
|
|
231
|
+
result.cyclesBroken += 1;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
seen.add(cur);
|
|
235
|
+
const next = byId.get(cur);
|
|
236
|
+
if (!next?.entry)
|
|
237
|
+
break;
|
|
238
|
+
prev = next;
|
|
239
|
+
cur = parentIdOf(next.entry);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const anyChange = result.removedPending > 0 || result.renamedIds > 0 || result.rewiredParents > 0 || result.cyclesBroken > 0;
|
|
244
|
+
if (!anyChange)
|
|
245
|
+
return result;
|
|
246
|
+
result.changed = true;
|
|
247
|
+
result.interrupted = result.removedPending > 0;
|
|
248
|
+
const out = [];
|
|
249
|
+
for (const s of slots) {
|
|
250
|
+
if (s.removed)
|
|
251
|
+
continue;
|
|
252
|
+
out.push(s.dirty && s.entry ? JSON.stringify(s.entry) : s.raw);
|
|
253
|
+
}
|
|
254
|
+
result.text = out.join("\n");
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 落盘版修复:健康文件只读不写;坏文件先写 <原名>.bak(已存在则保留最早那份,
|
|
259
|
+
* 不覆盖),再写回修复文本。读失败/写失败返回 null(调用方按原错误继续抛)。
|
|
260
|
+
*/
|
|
261
|
+
export function repairSessionFile(filePath) {
|
|
262
|
+
let raw;
|
|
263
|
+
try {
|
|
264
|
+
raw = readFileSync(filePath, "utf8");
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const r = repairSessionTranscript(raw);
|
|
270
|
+
if (!r.changed)
|
|
271
|
+
return { ...r, file: filePath, backup: null };
|
|
272
|
+
try {
|
|
273
|
+
const bak = `${filePath}.bak`;
|
|
274
|
+
if (!existsSync(bak))
|
|
275
|
+
writeFileSync(bak, raw, "utf8");
|
|
276
|
+
writeFileSync(filePath, r.text, "utf8");
|
|
277
|
+
return { ...r, file: filePath, backup: bak };
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** 链损坏的典型报错(SDK getBranch 死循环 → V8 RangeError)。 */
|
|
284
|
+
export function looksLikeChainCorruption(err) {
|
|
285
|
+
const msg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
286
|
+
return /invalid array length|maximum call stack|too much recursion/i.test(msg);
|
|
287
|
+
}
|