dsh-trivium 0.2.0 → 0.4.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/README.md +122 -79
- package/lib/client.js +1443 -2
- package/lib/episode.js +278 -0
- package/lib/extract.js +39 -7
- package/lib/index.js +127 -39
- package/lib/markdown.js +359 -0
- package/lib/pins.js +155 -0
- package/lib/schema.js +5 -0
- package/lib/server.js +113 -0
- package/lib/settings.js +26 -0
- package/lib/store.js +36 -8
- package/package.json +8 -3
package/lib/episode.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/** Compaction checkpoints and fork lineage as episode nodes. Not recalled by find. */
|
|
2
|
+
|
|
3
|
+
import { EDGE_LABELS, nowIso } from "./schema.js";
|
|
4
|
+
import { ensureLink, findByType, findByUri, insertNode, updateNode } from "./store.js";
|
|
5
|
+
|
|
6
|
+
const FALLBACK_SUMMARY = "已压缩";
|
|
7
|
+
const TAIL_SUMMARY = "当前";
|
|
8
|
+
const FORK_SUMMARY = "分叉";
|
|
9
|
+
|
|
10
|
+
/** Auto titles. A hand-set name that is not one of these is the box title. */
|
|
11
|
+
export const GENERIC_EPISODE_NAMES = Object.freeze([TAIL_SUMMARY, FALLBACK_SUMMARY, FORK_SUMMARY]);
|
|
12
|
+
|
|
13
|
+
export function isGenericEpisodeName(name) {
|
|
14
|
+
const n = String(name || "").trim();
|
|
15
|
+
return !n || GENERIC_EPISODE_NAMES.includes(n);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function genericNameOf(kind) {
|
|
19
|
+
if (kind === "tail") return TAIL_SUMMARY;
|
|
20
|
+
if (kind === "fork") return FORK_SUMMARY;
|
|
21
|
+
return FALLBACK_SUMMARY;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function episodeUri(sessionId, atSeq) {
|
|
25
|
+
return `ctx://episode/${sessionId}/${atSeq}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function tailUri(sessionId) {
|
|
29
|
+
return `ctx://episode/${sessionId}/tail`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sessionOf(payload) {
|
|
33
|
+
return String(payload?.sessionId || "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function listEpisodeRecords(db, sessionId) {
|
|
37
|
+
const want = sessionId == null || sessionId === "" ? null : String(sessionId);
|
|
38
|
+
const rows = [];
|
|
39
|
+
const fromIndex = findByType(db, "episode");
|
|
40
|
+
const ids = fromIndex.length
|
|
41
|
+
? fromIndex.map((n) => n.id)
|
|
42
|
+
: db.allNodeIds?.() || [];
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
for (const raw of ids) {
|
|
45
|
+
const id = Number(raw?.id ?? raw);
|
|
46
|
+
if (!Number.isFinite(id) || seen.has(id)) continue;
|
|
47
|
+
seen.add(id);
|
|
48
|
+
const node = db.get(id);
|
|
49
|
+
const payload = node?.payload || {};
|
|
50
|
+
if (payload.type !== "episode" || payload.status === "archived") continue;
|
|
51
|
+
if (want && sessionOf(payload) !== want) continue;
|
|
52
|
+
rows.push(recordOf(id, payload, node?.edges || []));
|
|
53
|
+
}
|
|
54
|
+
return rows.sort((a, b) => (a.atSeq || 0) - (b.atSeq || 0) || a.id - b.id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function recordOf(id, payload, edges) {
|
|
58
|
+
const summary = String(payload.summary || payload.text || "").trim();
|
|
59
|
+
return {
|
|
60
|
+
id: Number(id),
|
|
61
|
+
type: "episode",
|
|
62
|
+
sessionId: sessionOf(payload),
|
|
63
|
+
kind: payload.kind || "checkpoint",
|
|
64
|
+
atSeq: Number.isFinite(Number(payload.atSeq)) ? Number(payload.atSeq) : 0,
|
|
65
|
+
summary,
|
|
66
|
+
compactionId: payload.compactionId || "",
|
|
67
|
+
status: payload.status || "active",
|
|
68
|
+
createdAt: payload.createdAt || "",
|
|
69
|
+
updatedAt: payload.updatedAt || "",
|
|
70
|
+
uri: payload.uri || "",
|
|
71
|
+
name: payload.name || "",
|
|
72
|
+
text: payload.text || summary,
|
|
73
|
+
edges,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function upsertEpisode(db, payload) {
|
|
78
|
+
const uri = String(payload.uri || "").trim();
|
|
79
|
+
let id = uri ? findByUri(db, uri) : null;
|
|
80
|
+
if (id == null && payload.kind === "tail" && payload.sessionId) {
|
|
81
|
+
const existing = listEpisodeRecords(db, payload.sessionId).find((e) => e.kind === "tail");
|
|
82
|
+
if (existing) id = existing.id;
|
|
83
|
+
}
|
|
84
|
+
const body = {
|
|
85
|
+
type: "episode",
|
|
86
|
+
kind: payload.kind,
|
|
87
|
+
sessionId: String(payload.sessionId || ""),
|
|
88
|
+
atSeq: Number.isFinite(Number(payload.atSeq)) ? Number(payload.atSeq) : 0,
|
|
89
|
+
summary: String(payload.summary || payload.text || "").trim(),
|
|
90
|
+
compactionId: payload.compactionId || undefined,
|
|
91
|
+
uri,
|
|
92
|
+
name: payload.name || genericNameOf(payload.kind),
|
|
93
|
+
text: String(payload.summary || payload.text || "").trim(),
|
|
94
|
+
status: "active",
|
|
95
|
+
updatedAt: nowIso(),
|
|
96
|
+
};
|
|
97
|
+
if (id != null) {
|
|
98
|
+
const old = db.get(Number(id))?.payload || {};
|
|
99
|
+
const next = {
|
|
100
|
+
...old,
|
|
101
|
+
...body,
|
|
102
|
+
name: old.name || body.name,
|
|
103
|
+
kind: old.kind === "checkpoint" ? "checkpoint" : body.kind,
|
|
104
|
+
createdAt: old.createdAt || nowIso(),
|
|
105
|
+
};
|
|
106
|
+
if (payload.kind === "checkpoint") next.kind = "checkpoint";
|
|
107
|
+
db.updatePayload(Number(id), next);
|
|
108
|
+
db.flush();
|
|
109
|
+
return Number(id);
|
|
110
|
+
}
|
|
111
|
+
return insertNode(db, { ...body, createdAt: nowIso() });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function linkChain(db, sessionId) {
|
|
115
|
+
const rows = listEpisodeRecords(db, sessionId);
|
|
116
|
+
const checks = rows
|
|
117
|
+
.filter((e) => e.kind === "checkpoint" || e.kind === "fork")
|
|
118
|
+
.sort((a, b) => (a.atSeq || 0) - (b.atSeq || 0) || a.id - b.id);
|
|
119
|
+
const tail = rows.find((e) => e.kind === "tail");
|
|
120
|
+
for (let i = 0; i < checks.length - 1; i += 1) {
|
|
121
|
+
ensureLink(db, checks[i].id, checks[i + 1].id, EDGE_LABELS.continues);
|
|
122
|
+
}
|
|
123
|
+
if (tail && checks.length) {
|
|
124
|
+
ensureLink(db, checks[checks.length - 1].id, tail.id, EDGE_LABELS.continues);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** At most one tail episode per session. Stable uri so compaction can reuse the node. */
|
|
129
|
+
export function ensureTail(db, sessionId, { atSeq, summary } = {}) {
|
|
130
|
+
const sid = String(sessionId || "");
|
|
131
|
+
if (!sid) return null;
|
|
132
|
+
const existing = listEpisodeRecords(db, sid).find((e) => e.kind === "tail");
|
|
133
|
+
let text;
|
|
134
|
+
if (summary === undefined) {
|
|
135
|
+
text = existing && !isGenericEpisodeName(existing.summary) ? existing.summary : "";
|
|
136
|
+
} else {
|
|
137
|
+
text = String(summary || "").trim();
|
|
138
|
+
}
|
|
139
|
+
const id = upsertEpisode(db, {
|
|
140
|
+
kind: "tail",
|
|
141
|
+
sessionId: sid,
|
|
142
|
+
atSeq: Number.isFinite(Number(atSeq)) ? Number(atSeq) : existing?.atSeq || 0,
|
|
143
|
+
summary: text,
|
|
144
|
+
uri: tailUri(sid),
|
|
145
|
+
name: existing?.name || TAIL_SUMMARY,
|
|
146
|
+
});
|
|
147
|
+
linkChain(db, sid);
|
|
148
|
+
return id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function recordCheckpoint(db, { sessionId, atSeq, compactionId, summary } = {}) {
|
|
152
|
+
const sid = String(sessionId || "");
|
|
153
|
+
const seq = Number(atSeq);
|
|
154
|
+
if (!sid || !Number.isFinite(seq)) return null;
|
|
155
|
+
const text = String(summary || "").trim() || FALLBACK_SUMMARY;
|
|
156
|
+
const id = upsertEpisode(db, {
|
|
157
|
+
kind: "checkpoint",
|
|
158
|
+
sessionId: sid,
|
|
159
|
+
atSeq: seq,
|
|
160
|
+
compactionId: compactionId ? String(compactionId) : undefined,
|
|
161
|
+
summary: text,
|
|
162
|
+
uri: episodeUri(sid, seq),
|
|
163
|
+
});
|
|
164
|
+
ensureTail(db, sid, { atSeq: seq, summary: "" });
|
|
165
|
+
return id;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isCheckpointPayload(payload) {
|
|
169
|
+
return payload?.type === "episode" && payload.kind === "checkpoint";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parentCutId(db, parent, seq) {
|
|
173
|
+
if (seq != null) {
|
|
174
|
+
const existing = findByUri(db, episodeUri(parent, seq));
|
|
175
|
+
if (existing != null && isCheckpointPayload(db.get(existing)?.payload)) return existing;
|
|
176
|
+
}
|
|
177
|
+
const parentRows = listEpisodeRecords(db, parent);
|
|
178
|
+
if (seq != null) {
|
|
179
|
+
const hit = [...parentRows]
|
|
180
|
+
.reverse()
|
|
181
|
+
.find((e) => e.kind === "checkpoint" && (e.atSeq || 0) <= seq);
|
|
182
|
+
if (hit) return hit.id;
|
|
183
|
+
}
|
|
184
|
+
const lastCheck = [...parentRows].reverse().find((e) => e.kind === "checkpoint");
|
|
185
|
+
return lastCheck?.id ?? parentRows.find((e) => e.kind === "tail")?.id ?? null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Project a native or canvas fork onto the graph.
|
|
190
|
+
* Branch from an existing checkpoint or the parent's tail — never insert a dummy cut box.
|
|
191
|
+
*/
|
|
192
|
+
export function syncForkLineage(db, { childSessionId, parentSessionId, atSeq } = {}) {
|
|
193
|
+
const child = String(childSessionId || "");
|
|
194
|
+
const parent = String(parentSessionId || "");
|
|
195
|
+
if (!child || !parent) return null;
|
|
196
|
+
const seq = Number.isFinite(Number(atSeq)) ? Number(atSeq) : null;
|
|
197
|
+
const cutId = parentCutId(db, parent, seq);
|
|
198
|
+
const childTail = ensureTail(db, child, { atSeq: seq ?? 0 });
|
|
199
|
+
if (cutId != null && childTail != null) {
|
|
200
|
+
ensureLink(db, childTail, cutId, EDGE_LABELS.forksFrom);
|
|
201
|
+
}
|
|
202
|
+
linkChain(db, parent);
|
|
203
|
+
return { cutId, childTail };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function sessionMapSnapshot(db, sessionId) {
|
|
207
|
+
const sid = String(sessionId || "");
|
|
208
|
+
const all = listEpisodeRecords(db);
|
|
209
|
+
const mineIds = new Set(all.filter((n) => n.sessionId === sid).map((n) => n.id));
|
|
210
|
+
const edges = [];
|
|
211
|
+
for (const n of all) {
|
|
212
|
+
const node = db.get(n.id);
|
|
213
|
+
for (const e of node?.edges || []) {
|
|
214
|
+
const label = e.label || "";
|
|
215
|
+
if (label !== EDGE_LABELS.continues && label !== EDGE_LABELS.forksFrom) continue;
|
|
216
|
+
const to = Number(e.targetId ?? e.target_id);
|
|
217
|
+
if (!Number.isFinite(to)) continue;
|
|
218
|
+
edges.push({ from: n.id, to, label });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const related = new Set(mineIds);
|
|
222
|
+
let grew = true;
|
|
223
|
+
while (grew) {
|
|
224
|
+
grew = false;
|
|
225
|
+
for (const e of edges) {
|
|
226
|
+
if (related.has(e.from) && !related.has(e.to)) {
|
|
227
|
+
related.add(e.to);
|
|
228
|
+
grew = true;
|
|
229
|
+
}
|
|
230
|
+
if (related.has(e.to) && !related.has(e.from)) {
|
|
231
|
+
related.add(e.from);
|
|
232
|
+
grew = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const nodes = all
|
|
237
|
+
.filter((n) => related.has(n.id) || n.sessionId === sid)
|
|
238
|
+
.map((n) => ({
|
|
239
|
+
id: n.id,
|
|
240
|
+
sessionId: n.sessionId,
|
|
241
|
+
kind: n.kind,
|
|
242
|
+
atSeq: n.atSeq,
|
|
243
|
+
summary: n.summary,
|
|
244
|
+
compactionId: n.compactionId,
|
|
245
|
+
status: n.status,
|
|
246
|
+
createdAt: n.createdAt,
|
|
247
|
+
uri: n.uri,
|
|
248
|
+
name: n.name || "",
|
|
249
|
+
current: n.sessionId === sid && n.kind === "tail",
|
|
250
|
+
}));
|
|
251
|
+
return {
|
|
252
|
+
sessionId: sid,
|
|
253
|
+
nodes,
|
|
254
|
+
edges: edges.filter((e) => related.has(e.from) && related.has(e.to)),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Hand-rename a box. Writes only `name`. Rejects non-episode ids.
|
|
260
|
+
* Callers must not pass text/summary into updateNode.
|
|
261
|
+
*/
|
|
262
|
+
export function renameEpisode(db, id, name) {
|
|
263
|
+
const nid = Number(id);
|
|
264
|
+
if (!Number.isFinite(nid)) return { ok: false, status: 400, message: "bad id" };
|
|
265
|
+
const node = db.get(nid);
|
|
266
|
+
if (!node) return { ok: false, status: 404, message: "not found" };
|
|
267
|
+
const payload = node.payload || {};
|
|
268
|
+
if (payload.type !== "episode") {
|
|
269
|
+
return { ok: false, status: 400, message: "not an episode" };
|
|
270
|
+
}
|
|
271
|
+
const next = updateNode(db, nid, { name: String(name ?? "").trim() });
|
|
272
|
+
return {
|
|
273
|
+
ok: true,
|
|
274
|
+
id: nid,
|
|
275
|
+
name: next?.name || "",
|
|
276
|
+
summary: String(next?.summary || payload.summary || payload.text || ""),
|
|
277
|
+
};
|
|
278
|
+
}
|
package/lib/extract.js
CHANGED
|
@@ -18,6 +18,11 @@ export const ONESHOT_CUES =
|
|
|
18
18
|
export const CHITCHAT_CUES =
|
|
19
19
|
/^(哈哈+|呵呵+|嗯+|哦+|好的|谢谢|天气|吃了吗|在吗)[\s!!.。??]*$/;
|
|
20
20
|
|
|
21
|
+
/** WorkBuddy-style per-batch write cap. Not a file-size limit. */
|
|
22
|
+
export const MAX_WRITE_CHARS = 3000;
|
|
23
|
+
export const MAX_LINE_CHARS = 240;
|
|
24
|
+
export const MAX_WRITE_ITEMS = 24;
|
|
25
|
+
|
|
21
26
|
const NAME_RE =
|
|
22
27
|
/`([^`]{2,64})`|\b([A-Z][a-z]+[A-Z][A-Za-z0-9]*)\b|\b([A-Z][A-Za-z0-9]*(?:[_-][A-Z][A-Za-z0-9]*)+)\b|\b([a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)+)\b|\b([A-Z]{3,}[A-Z0-9_-]{0,})\b/g;
|
|
23
28
|
|
|
@@ -170,6 +175,10 @@ function linkNameOf(span, turnText, uniqueTurn, { inSpanOnly = false } = {}) {
|
|
|
170
175
|
);
|
|
171
176
|
}
|
|
172
177
|
|
|
178
|
+
export function firstEntityName(text) {
|
|
179
|
+
return collectNames(text, 1)[0]?.name;
|
|
180
|
+
}
|
|
181
|
+
|
|
173
182
|
function collectNames(text, min = 2) {
|
|
174
183
|
const counts = new Map();
|
|
175
184
|
const s = String(text || "");
|
|
@@ -298,10 +307,12 @@ export function ruleCandidates(turns) {
|
|
|
298
307
|
return out;
|
|
299
308
|
}
|
|
300
309
|
|
|
301
|
-
export function filterCandidates(candidates) {
|
|
310
|
+
export function filterCandidates(candidates, { importFile = false } = {}) {
|
|
302
311
|
const seen = new Set();
|
|
303
312
|
const out = [];
|
|
304
|
-
const caps =
|
|
313
|
+
const caps = importFile
|
|
314
|
+
? { entity: 24, preference: 24, decision: 12, experience: 8 }
|
|
315
|
+
: { entity: 8, preference: 8, decision: 6, experience: 4 };
|
|
305
316
|
const used = { entity: 0, preference: 0, decision: 0, experience: 0 };
|
|
306
317
|
for (const raw of candidates || []) {
|
|
307
318
|
if (!raw || !WRITE_TYPES.has(raw.type)) continue;
|
|
@@ -315,7 +326,7 @@ export function filterCandidates(candidates) {
|
|
|
315
326
|
const blob = `${raw.text || ""} ${raw.quote || ""}`;
|
|
316
327
|
if (CHITCHAT_CUES.test(String(raw.text || "").trim())) continue;
|
|
317
328
|
if (ONESHOT_CUES.test(blob) && !PREFERENCE_CUES.test(blob)) continue;
|
|
318
|
-
if (!PREFERENCE_CUES.test(blob)) continue;
|
|
329
|
+
if (!importFile && !PREFERENCE_CUES.test(blob)) continue;
|
|
319
330
|
}
|
|
320
331
|
if ((used[raw.type] || 0) >= (caps[raw.type] || 4)) continue;
|
|
321
332
|
const key = `${raw.type}|${(raw.name || "").toLowerCase()}|${text.toLowerCase()}`;
|
|
@@ -325,7 +336,7 @@ export function filterCandidates(candidates) {
|
|
|
325
336
|
out.push({
|
|
326
337
|
type: raw.type,
|
|
327
338
|
name: raw.name ? String(raw.name).slice(0, 80) : undefined,
|
|
328
|
-
text: clip(text,
|
|
339
|
+
text: clip(text, MAX_LINE_CHARS),
|
|
329
340
|
linkName: raw.linkName ? String(raw.linkName).slice(0, 80) : undefined,
|
|
330
341
|
linkLabel: raw.linkLabel || undefined,
|
|
331
342
|
fail: raw.fail ? clip(raw.fail, 200) : undefined,
|
|
@@ -377,8 +388,29 @@ function resolveEntityId(db, name) {
|
|
|
377
388
|
return findMergeTarget(db, { type: "entity", name, text: name });
|
|
378
389
|
}
|
|
379
390
|
|
|
380
|
-
|
|
381
|
-
|
|
391
|
+
/** Drop the tail once the WorkBuddy-style char/item budget is spent. */
|
|
392
|
+
export function capWriteBatch(candidates, { maxChars = MAX_WRITE_CHARS, maxItems = MAX_WRITE_ITEMS } = {}) {
|
|
393
|
+
const kept = [];
|
|
394
|
+
let used = 0;
|
|
395
|
+
for (const cand of candidates || []) {
|
|
396
|
+
if (kept.length >= maxItems) break;
|
|
397
|
+
const n = String(cand.text || cand.name || "").length;
|
|
398
|
+
if (used + n > maxChars && kept.length) break;
|
|
399
|
+
if (n > maxChars && !kept.length) {
|
|
400
|
+
kept.push({ ...cand, text: clip(cand.text || cand.name || "", MAX_LINE_CHARS) });
|
|
401
|
+
used += Math.min(n, MAX_LINE_CHARS);
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
kept.push(cand);
|
|
405
|
+
used += n;
|
|
406
|
+
}
|
|
407
|
+
return { kept, used, dropped: Math.max(0, (candidates || []).length - kept.length) };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function applyCandidates(db, candidates, { sessionId, via } = {}) {
|
|
411
|
+
const { kept: list } = capWriteBatch(
|
|
412
|
+
filterCandidates(candidates, { importFile: via === "workbuddy-import" }),
|
|
413
|
+
);
|
|
382
414
|
const ordered = [
|
|
383
415
|
...list.filter((c) => c.type === "entity"),
|
|
384
416
|
...list.filter((c) => c.type !== "entity"),
|
|
@@ -398,7 +430,7 @@ export function applyCandidates(db, candidates, { sessionId } = {}) {
|
|
|
398
430
|
source: {
|
|
399
431
|
sessionId: sessionId ? String(sessionId) : "",
|
|
400
432
|
quote: cand.quote,
|
|
401
|
-
via: "extract",
|
|
433
|
+
via: via || cand.via || "extract",
|
|
402
434
|
},
|
|
403
435
|
};
|
|
404
436
|
const untilAt = cand.until ? resolveUntilAt(cand.until) : undefined;
|
package/lib/index.js
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
import { readUiSettings, registerMemoryApi } from "./server.js";
|
|
23
23
|
import { applyRecallMode, recallModeOf } from "./settings.js";
|
|
24
24
|
import { configureEmbedding } from "./embed.js";
|
|
25
|
+
import { recordCheckpoint, ensureTail, syncForkLineage } from "./episode.js";
|
|
26
|
+
import { buildPinInject } from "./pins.js";
|
|
25
27
|
|
|
26
28
|
export const name = "dsh-trivium";
|
|
27
29
|
export const inject = ["tools"];
|
|
@@ -206,6 +208,15 @@ function onSessionEvent(session, event) {
|
|
|
206
208
|
if (buf.dirty && buf.cwd && buf.turns.length) {
|
|
207
209
|
savePending(buf.cwd, { sessionId: buf.sessionId, turns: buf.turns });
|
|
208
210
|
}
|
|
211
|
+
buf.lastTurnEndSeq = event.seq;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case "compaction/summary": {
|
|
215
|
+
buf.compactionSummary = {
|
|
216
|
+
id: event.data?.compactionId,
|
|
217
|
+
text: textFromContent(event.data?.summary),
|
|
218
|
+
end: event.data?.shadowedRange?.end,
|
|
219
|
+
};
|
|
209
220
|
break;
|
|
210
221
|
}
|
|
211
222
|
default:
|
|
@@ -245,6 +256,25 @@ export function apply(ctx, config = {}) {
|
|
|
245
256
|
},
|
|
246
257
|
}),
|
|
247
258
|
);
|
|
259
|
+
try {
|
|
260
|
+
const session = agent.session;
|
|
261
|
+
const sessionId = sessionKey(session);
|
|
262
|
+
if (sessionId) {
|
|
263
|
+
const seedLen = session?.header?.seedLength;
|
|
264
|
+
ensureTail(db, sessionId, {
|
|
265
|
+
atSeq: Number.isFinite(Number(seedLen)) && seedLen > 0 ? seedLen - 1 : 0,
|
|
266
|
+
});
|
|
267
|
+
if (session?.header?.parentSession) {
|
|
268
|
+
syncForkLineage(db, {
|
|
269
|
+
childSessionId: sessionId,
|
|
270
|
+
parentSessionId: session.header.parentSession,
|
|
271
|
+
atSeq: Number.isFinite(Number(seedLen)) && seedLen > 0 ? seedLen - 1 : undefined,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
log.warn?.(`[dsh-trivium] episode tail skipped: ${err.message}`);
|
|
277
|
+
}
|
|
248
278
|
const pending = loadPending(cwd);
|
|
249
279
|
if (pending?.turns?.length && optionsOf().extractEnabled !== false) {
|
|
250
280
|
runExtract(runtime, {
|
|
@@ -278,9 +308,45 @@ export function apply(ctx, config = {}) {
|
|
|
278
308
|
options: optionsOf(),
|
|
279
309
|
reason: "compaction/end",
|
|
280
310
|
}).catch((err) => log.warn?.(`[dsh-trivium] compaction extract: ${err.message}`));
|
|
311
|
+
const cwd = buf.cwd || session.header?.cwd;
|
|
312
|
+
const sessionId = buf.sessionId || session.id;
|
|
313
|
+
if (cwd && sessionId) {
|
|
314
|
+
const summary =
|
|
315
|
+
buf.compactionSummary?.id === event.data?.compactionId && buf.compactionSummary.text
|
|
316
|
+
? buf.compactionSummary.text
|
|
317
|
+
: "已压缩";
|
|
318
|
+
const atSeq = Number.isFinite(Number(event.seq))
|
|
319
|
+
? Number(event.seq)
|
|
320
|
+
: Number.isFinite(Number(buf.compactionSummary?.end))
|
|
321
|
+
? Number(buf.compactionSummary.end)
|
|
322
|
+
: Number(buf.lastTurnEndSeq) || 0;
|
|
323
|
+
openWorkspaceDb(cwd)
|
|
324
|
+
.then((db) => {
|
|
325
|
+
recordCheckpoint(db, {
|
|
326
|
+
sessionId,
|
|
327
|
+
atSeq,
|
|
328
|
+
compactionId: event.data?.compactionId,
|
|
329
|
+
summary,
|
|
330
|
+
});
|
|
331
|
+
})
|
|
332
|
+
.catch((err) => log.warn?.(`[dsh-trivium] episode checkpoint skipped: ${err.message}`));
|
|
333
|
+
}
|
|
281
334
|
}
|
|
282
335
|
if (event.type === "turn/end") {
|
|
283
336
|
const id = sessionKey(session);
|
|
337
|
+
const bufEnd = bufOf(session);
|
|
338
|
+
const lastUser = [...(bufEnd.turns || [])].reverse().find((t) => t.role === "user");
|
|
339
|
+
const preview = String(lastUser?.text || "").trim();
|
|
340
|
+
if (bufEnd.cwd && bufEnd.sessionId && preview && !preview.startsWith("/")) {
|
|
341
|
+
openWorkspaceDb(bufEnd.cwd)
|
|
342
|
+
.then((db) => {
|
|
343
|
+
ensureTail(db, bufEnd.sessionId, {
|
|
344
|
+
atSeq: Number(event.seq) || bufEnd.lastTurnEndSeq || 0,
|
|
345
|
+
summary: preview.slice(0, 240),
|
|
346
|
+
});
|
|
347
|
+
})
|
|
348
|
+
.catch((err) => log.warn?.(`[dsh-trivium] episode tail preview skipped: ${err.message}`));
|
|
349
|
+
}
|
|
284
350
|
clearIdle(id);
|
|
285
351
|
const timer = setTimeout(() => {
|
|
286
352
|
idleTimers.delete(id);
|
|
@@ -310,50 +376,72 @@ export function apply(ctx, config = {}) {
|
|
|
310
376
|
ctx.on("agent/pre-step", async ({ agent, messages }, next) => {
|
|
311
377
|
const decision = await next();
|
|
312
378
|
try {
|
|
313
|
-
const options = optionsOf();
|
|
314
|
-
const mode = recallModeOf(options);
|
|
315
|
-
if (mode === "off") return decision;
|
|
316
379
|
if (!decision || decision.kind !== "enter") return decision;
|
|
317
|
-
const
|
|
318
|
-
const lastUser = [...proposed].reverse().find((m) => m?.source?.kind === "user");
|
|
319
|
-
if (!lastUser) return decision;
|
|
320
|
-
const query = textFromContent(lastUser.content).trim();
|
|
321
|
-
if (!query) return decision;
|
|
380
|
+
const extra = [];
|
|
322
381
|
const db = await openWorkspaceDb(cwdOf(agent));
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
382
|
+
const sessionId = sessionKey(agent.session);
|
|
383
|
+
try {
|
|
384
|
+
const pin = buildPinInject(db, sessionId, 300);
|
|
385
|
+
if (pin.text) {
|
|
386
|
+
extra.push(
|
|
387
|
+
createUserMessage({
|
|
388
|
+
content: [{ type: "text", text: pin.text }],
|
|
389
|
+
source: {
|
|
390
|
+
kind: "plugin",
|
|
391
|
+
plugin: "dsh-trivium",
|
|
392
|
+
form: "pins",
|
|
393
|
+
},
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
log.warn?.(`[dsh-trivium] pre-step pins skipped: ${err.message}`);
|
|
333
399
|
}
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
400
|
+
const options = optionsOf();
|
|
401
|
+
const mode = recallModeOf(options);
|
|
402
|
+
if (mode !== "off") {
|
|
403
|
+
const proposed = [...(decision.messages || []), ...(messages || [])];
|
|
404
|
+
const lastUser = [...proposed].reverse().find((m) => m?.source?.kind === "user");
|
|
405
|
+
const query = lastUser ? textFromContent(lastUser.content).trim() : "";
|
|
406
|
+
if (query) {
|
|
407
|
+
let hits;
|
|
408
|
+
if (mode === "anchor") {
|
|
409
|
+
hits = namedRecallHits(db, query, { topK: 8 }).slice(0, 8);
|
|
410
|
+
} else {
|
|
411
|
+
const queryVector = await embedQueryVector(query);
|
|
412
|
+
hits = searchNodes(db, query, {
|
|
413
|
+
topK: 3,
|
|
414
|
+
expandDepth: 1,
|
|
415
|
+
queryVector: queryVector || undefined,
|
|
416
|
+
}).slice(0, 3);
|
|
417
|
+
}
|
|
418
|
+
if (hits.length) {
|
|
419
|
+
const lines = hits.map((hit) => {
|
|
420
|
+
const row = formatHit(db, hit);
|
|
421
|
+
return `- [${row.type}#${row.id}] ${row.l0} (${row.path.join(" | ") || "no path"})`;
|
|
422
|
+
});
|
|
423
|
+
const form = mode === "anchor" ? "anchorRecall" : "autoRecall";
|
|
424
|
+
const text = clipToTokenBudget(
|
|
425
|
+
`dsh-trivium ${form} (L0 only; use ctx_read for full text):\n${lines.join("\n")}`,
|
|
426
|
+
300,
|
|
427
|
+
);
|
|
428
|
+
extra.push(
|
|
429
|
+
createUserMessage({
|
|
430
|
+
content: [{ type: "text", text }],
|
|
431
|
+
source: {
|
|
432
|
+
kind: "plugin",
|
|
433
|
+
plugin: "dsh-trivium",
|
|
434
|
+
form: "recall",
|
|
435
|
+
},
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (!extra.length) return decision;
|
|
344
442
|
return {
|
|
345
443
|
kind: "enter",
|
|
346
|
-
messages: [
|
|
347
|
-
createUserMessage({
|
|
348
|
-
content: [{ type: "text", text }],
|
|
349
|
-
source: {
|
|
350
|
-
kind: "plugin",
|
|
351
|
-
plugin: "dsh-trivium",
|
|
352
|
-
form: "recall",
|
|
353
|
-
},
|
|
354
|
-
}),
|
|
355
|
-
...decision.messages,
|
|
356
|
-
],
|
|
444
|
+
messages: [...extra, ...decision.messages],
|
|
357
445
|
};
|
|
358
446
|
} catch (err) {
|
|
359
447
|
log.warn?.(`[dsh-trivium] pre-step recall skipped: ${err.message}`);
|