dsh-lite-memory 0.3.0 → 0.15.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 +73 -17
- package/TRIGGERS.md +18 -14
- package/anchor.js +518 -0
- package/bench/bench.mjs +145 -0
- package/bench/evolution.mjs +123 -0
- package/client.js +1519 -33
- package/docs/BENCH.md +74 -0
- package/docs/RELEASE-0.11.0.md +27 -0
- package/docs/RELEASE-0.11.1.md +11 -0
- package/docs/RELEASE-0.12.0.md +15 -0
- package/docs/RELEASE-0.12.1.md +17 -0
- package/docs/RELEASE-0.12.2.md +17 -0
- package/docs/RELEASE-0.13.0.md +7 -0
- package/docs/RELEASE-0.14.0.md +7 -0
- package/docs/RELEASE-0.15.0.md +8 -0
- package/index.js +2080 -76
- package/learning.js +585 -0
- package/package.json +10 -2
- package/state.js +1476 -0
- package/store.js +2967 -244
- package/test/anchor.test.mjs +436 -0
- package/test/client.test.mjs +114 -0
- package/test/contract.test.mjs +880 -0
- package/test/control.test.mjs +278 -0
- package/test/crossing.test.mjs +372 -0
- package/test/evolution-read.test.mjs +448 -0
- package/test/fixtures/legacy-v1/archive/mem_444444444444.json +18 -0
- package/test/fixtures/legacy-v1/entries/mem_111111111111.md +2 -0
- package/test/fixtures/legacy-v1/entries/mem_222222222222.md +1 -0
- package/test/fixtures/legacy-v1/entries/mem_333333333333.md +1 -0
- package/test/fixtures/legacy-v1/index.json +63 -0
- package/test/learning.test.mjs +772 -0
- package/test/lexical.test.mjs +33 -0
- package/test/recall.test.mjs +449 -0
- package/test/scenario.test.mjs +184 -0
- package/test/state.test.mjs +485 -0
- package/test/store.test.mjs +816 -115
- package/test/view.test.mjs +257 -0
- package/view.js +616 -0
package/anchor.js
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 任务锚点与请求快照的身份规则都在这里:纯函数,不摸盘、不调模型。
|
|
5
|
+
* 无法确定时只留 unbound 作品壳;栏目默认必须由用户显式确认,不能按“只有一个”猜。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const ANCHOR_STATUSES = ['unbound', 'proposed', 'active', 'continuable', 'ambiguous', 'closed'];
|
|
9
|
+
export const TASK_LIMITS = { sessions: 8, episodes: 12, sourceMessages: 8, idleMs: 7 * 24 * 60 * 60 * 1000, indexBytes: 600 };
|
|
10
|
+
|
|
11
|
+
const SWITCH_WORDS = /(改做|改为|改到|改成|换个|换一个|换成|转到|现在做|接下来做|新做|另做|开始做|去做)/;
|
|
12
|
+
const CONTINUE_WORDS = /(继续|再做一期|下一期|再来一期|再开一期|续一集|再拍一期)/;
|
|
13
|
+
const REVISE_WORDS = /(再改一下|改一版|再改改|调整一下|这版再|继续改)/;
|
|
14
|
+
const THANKS_WORDS = /^(谢谢|多谢|谢了|辛苦了|thanks|thank\s*you)[!!。.~~\s]*$/i;
|
|
15
|
+
const REFERENCE_WORDS = /(这个|这条|这次|上次|之前|刚才|上面|前面|它|这里|继续|接着|还是|同样|再改)/;
|
|
16
|
+
const TASK_STOP = new Set(['一个','一下','这个','那个','我们','你们','他们','什么','怎么','可以','需要','帮我','帮忙','请问','然后','现在','还是','就是','没有','不是','以及','关于','进行','使用','问题','任务','内容','时候','已经','如果','因为','所以','但是','而且','或者','这些','那些','这样','那样','一下','一点','一些']);
|
|
17
|
+
/** 判断任务是否换了对象时,这些词只表示动作或指代,不算对象证据。 */
|
|
18
|
+
const TASK_NOISE = /(继续|再做一期|下一期|再来一期|再开一期|续一集|再拍一期|接着|然后|接下来|现在|帮我|帮忙|这个|这条|这次|上次|之前|刚才|上面|前面|下面|后面|这里|还是|同样|再改|排查|处理|检查|看看|看一下|看下|修复|解决|优化|调整|修改|分析|研究|对比|评估|调研|测试|设计|实现|开发|搭建|部署|定位|复现|查一下|找一下|问题|任务|内容|一下)/g;
|
|
19
|
+
const NEW_OBJECT_VERB = /(排查|检查|看看|看一下|看下|分析|研究|对比|评估|调研|测试|设计|实现|开发|搭建|部署|修复|解决|处理|定位|复现|查一下|找一下)/;
|
|
20
|
+
const NEW_TASK_MARKER = /(现在|接下来|然后|下面|新任务|另一个|重新|改做|换个|换一个)/;
|
|
21
|
+
/** 续接词后面还得跟一个动作,才算真正在改旧稿;单说「继续 / 这个 / 之前」不构成复用依据。 */
|
|
22
|
+
const CONTINUATION_ACTION = /(继续|接着|然后|再|还是|同样)\s*(做|改|调|弄|看|写|处理|优化|调整|检查|完成)/;
|
|
23
|
+
function taskTerms(value) {
|
|
24
|
+
const plain = text(value, 200).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').replace(TASK_NOISE, ' ').replace(/\s+/g, ' ').trim();
|
|
25
|
+
const terms = new Set();
|
|
26
|
+
for (const word of plain.match(/[a-z][a-z0-9_-]{2,}/g) || []) terms.add(word);
|
|
27
|
+
const runs = plain.match(/[\u4e00-\u9fff]+/g) || [];
|
|
28
|
+
for (const run of runs) {
|
|
29
|
+
if (TASK_STOP.has(run)) continue;
|
|
30
|
+
for (let i = 0; i + 2 <= run.length; i += 1) {
|
|
31
|
+
const gram = run.slice(i, i + 2);
|
|
32
|
+
if (!TASK_STOP.has(gram)) terms.add(gram);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return terms;
|
|
36
|
+
}
|
|
37
|
+
/** 继承旧任务要有正面证据:新要求与本轮任务没有实质对象交集时,先按新任务处理。 */
|
|
38
|
+
export function looksLikeNewTask(query, previousText) {
|
|
39
|
+
const current = text(query, 200);
|
|
40
|
+
if (!current || isThanks(current) || isRevision(current)) return false;
|
|
41
|
+
const next = taskTerms(current);
|
|
42
|
+
const previous = taskTerms(previousText);
|
|
43
|
+
const cue = isContinuation(current) || REFERENCE_WORDS.test(current);
|
|
44
|
+
/** 只剩指代或续接词时,必须另有动作才算续接;否则宁可换任务也不沿用旧范围。 */
|
|
45
|
+
if (!next.size) return cue ? !CONTINUATION_ACTION.test(current) : false;
|
|
46
|
+
if (!previous.size) return false;
|
|
47
|
+
for (const term of next) if (previous.has(term)) return false;
|
|
48
|
+
/** 只有指代或「继续」时,还要有明确的新对象动作、转向词或新英文对象才换任务。 */
|
|
49
|
+
if (!cue) return true;
|
|
50
|
+
if (NEW_OBJECT_VERB.test(current) || NEW_TASK_MARKER.test(current)) return true;
|
|
51
|
+
for (const term of next) if (/^[a-z]/.test(term)) return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function emptyTasks() {
|
|
56
|
+
return { revision: 0, defaults: null, sessions: {}, episodes: {} };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function text(value, max = 120) {
|
|
60
|
+
return typeof value === 'string' ? value.normalize('NFKC').replace(/\s+/g, ' ').trim().slice(0, max) : '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function clone(value) {
|
|
64
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isThanks(query) {
|
|
68
|
+
return THANKS_WORDS.test(text(query, 40));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isContinuation(query) {
|
|
72
|
+
return CONTINUE_WORDS.test(text(query, 160));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isRevision(query) {
|
|
76
|
+
return REVISE_WORDS.test(text(query, 160));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function requestBatchKey(messageIds) {
|
|
80
|
+
const ids = [...new Set((Array.isArray(messageIds) ? messageIds : []).map((id) => String(id || '')).filter(Boolean))].sort();
|
|
81
|
+
if (!ids.length) return '';
|
|
82
|
+
return `rb_${crypto.createHash('sha256').update(ids.join('\u0000')).digest('hex').slice(0, 16)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hash(value, chars = 12) {
|
|
86
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, chars);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function transitionIdFor(batchKey, previousRevision = 0) {
|
|
90
|
+
return `tr_${hash(`${batchKey}\u0000${Number(previousRevision) || 0}`)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function episodeIdFor(transitionId) {
|
|
94
|
+
return `ep_${hash(`episode\u0000${transitionId}`)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** contextId 只在本请求内有效;前缀带项目摘要,跨项目一眼就能拒绝。 */
|
|
98
|
+
export function contextIdFor(projectId, batchKey, nonce) {
|
|
99
|
+
return `ctx_${hash(`project\u0000${projectId}`, 8)}_${hash(`request\u0000${batchKey}\u0000${nonce || ''}`, 8)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function projectTagOf(contextId) {
|
|
103
|
+
const match = /^ctx_([a-f0-9]{8})_/.exec(String(contextId || ''));
|
|
104
|
+
return match ? match[1] : '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function projectTag(projectId) {
|
|
108
|
+
return hash(`project\u0000${projectId}`, 8);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function normalizeTasks(raw) {
|
|
112
|
+
const base = emptyTasks();
|
|
113
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base;
|
|
114
|
+
const output = base;
|
|
115
|
+
output.revision = Number.isInteger(raw.revision) && raw.revision > 0 ? raw.revision : 0;
|
|
116
|
+
if (raw.defaults && typeof raw.defaults === 'object') {
|
|
117
|
+
const taskKind = text(raw.defaults.taskKind, 40);
|
|
118
|
+
const seriesId = text(raw.defaults.seriesId, 40);
|
|
119
|
+
const phase = text(raw.defaults.phase, 40);
|
|
120
|
+
if (taskKind || seriesId || phase) {
|
|
121
|
+
output.defaults = {
|
|
122
|
+
...(taskKind ? { taskKind } : {}),
|
|
123
|
+
...(seriesId ? { seriesId } : {}),
|
|
124
|
+
...(phase ? { phase } : {}),
|
|
125
|
+
confirmedBy: 'user',
|
|
126
|
+
confirmedAt: text(raw.defaults.confirmedAt, 40),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const sessions = raw.sessions && typeof raw.sessions === 'object' ? raw.sessions : {};
|
|
131
|
+
for (const [sessionId, anchor] of Object.entries(sessions)) {
|
|
132
|
+
if (!sessionId || !anchor || typeof anchor !== 'object') continue;
|
|
133
|
+
const status = ANCHOR_STATUSES.includes(anchor.status) ? anchor.status : 'unbound';
|
|
134
|
+
output.sessions[sessionId] = {
|
|
135
|
+
episodeId: text(anchor.episodeId, 80),
|
|
136
|
+
batchKey: text(anchor.batchKey, 40),
|
|
137
|
+
...(text(anchor.taskKind, 40) ? { taskKind: text(anchor.taskKind, 40) } : {}),
|
|
138
|
+
...(text(anchor.seriesId, 40) ? { seriesId: text(anchor.seriesId, 40) } : {}),
|
|
139
|
+
...(text(anchor.phase, 40) ? { phase: text(anchor.phase, 40) } : {}),
|
|
140
|
+
status,
|
|
141
|
+
bindingRevision: Number.isInteger(anchor.bindingRevision) && anchor.bindingRevision > 0 ? anchor.bindingRevision : 1,
|
|
142
|
+
sourceMessageIds: (Array.isArray(anchor.sourceMessageIds) ? anchor.sourceMessageIds : []).map((id) => text(id, 120)).filter(Boolean).slice(0, TASK_LIMITS.sourceMessages),
|
|
143
|
+
updatedAt: text(anchor.updatedAt, 40),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const episodes = raw.episodes && typeof raw.episodes === 'object' ? raw.episodes : {};
|
|
147
|
+
for (const [episodeId, episode] of Object.entries(episodes)) {
|
|
148
|
+
if (!episodeId || !episode || typeof episode !== 'object') continue;
|
|
149
|
+
output.episodes[episodeId] = {
|
|
150
|
+
episodeId,
|
|
151
|
+
...(text(episode.taskKind, 40) ? { taskKind: text(episode.taskKind, 40) } : {}),
|
|
152
|
+
...(text(episode.seriesId, 40) ? { seriesId: text(episode.seriesId, 40) } : {}),
|
|
153
|
+
...(text(episode.phase, 40) ? { phase: text(episode.phase, 40) } : {}),
|
|
154
|
+
status: ANCHOR_STATUSES.includes(episode.status) ? episode.status : 'unbound',
|
|
155
|
+
transitionId: text(episode.transitionId, 80),
|
|
156
|
+
previousEpisodeId: text(episode.previousEpisodeId, 80),
|
|
157
|
+
...(text(episode.taskText, 200) ? { taskText: text(episode.taskText, 200) } : {}),
|
|
158
|
+
createdAt: text(episode.createdAt, 40),
|
|
159
|
+
updatedAt: text(episode.updatedAt, 40),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return pruneTasks(output);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 只保留最近的表和作品壳:项目状态有硬上限,读路径不能越用越肥。 */
|
|
166
|
+
export function pruneTasks(tasks, now = Date.now()) {
|
|
167
|
+
const out = normalizeTasksShallow(tasks);
|
|
168
|
+
const sessions = Object.entries(out.sessions)
|
|
169
|
+
.sort((a, b) => String(b[1].updatedAt).localeCompare(String(a[1].updatedAt)))
|
|
170
|
+
.slice(0, TASK_LIMITS.sessions);
|
|
171
|
+
out.sessions = Object.fromEntries(sessions);
|
|
172
|
+
const live = new Set(Object.values(out.sessions).map((anchor) => anchor.episodeId).filter(Boolean));
|
|
173
|
+
const episodes = Object.entries(out.episodes)
|
|
174
|
+
.filter(([id, episode]) => live.has(id) || now - Date.parse(episode.updatedAt || episode.createdAt || 0) < TASK_LIMITS.idleMs)
|
|
175
|
+
.sort((a, b) => String(b[1].updatedAt).localeCompare(String(a[1].updatedAt)))
|
|
176
|
+
.slice(0, TASK_LIMITS.episodes);
|
|
177
|
+
out.episodes = Object.fromEntries(episodes);
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 防止 prune 调 normalize 无限递归;这里只做浅层结构复制。 */
|
|
182
|
+
function normalizeTasksShallow(raw) {
|
|
183
|
+
const base = emptyTasks();
|
|
184
|
+
if (!raw || typeof raw !== 'object') return base;
|
|
185
|
+
base.revision = Number.isInteger(raw.revision) ? raw.revision : 0;
|
|
186
|
+
base.defaults = raw.defaults && typeof raw.defaults === 'object' ? clone(raw.defaults) : null;
|
|
187
|
+
base.sessions = raw.sessions && typeof raw.sessions === 'object' ? clone(raw.sessions) : {};
|
|
188
|
+
base.episodes = raw.episodes && typeof raw.episodes === 'object' ? clone(raw.episodes) : {};
|
|
189
|
+
return base;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function focusOf(tasks, sessionId) {
|
|
193
|
+
const normalized = normalizeTasks(tasks);
|
|
194
|
+
return normalized.sessions[String(sessionId || '')] || null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function episodeOf(tasks, episodeId) {
|
|
198
|
+
return episodeId ? tasks.episodes[episodeId] || null : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function anchorOf(episode, { sessionId, batchKey, messageIds, status, bindingRevision, now }) {
|
|
202
|
+
return {
|
|
203
|
+
episodeId: episode.episodeId,
|
|
204
|
+
...(episode.taskKind ? { taskKind: episode.taskKind } : {}),
|
|
205
|
+
...(episode.seriesId ? { seriesId: episode.seriesId } : {}),
|
|
206
|
+
...(episode.phase ? { phase: episode.phase } : {}),
|
|
207
|
+
status,
|
|
208
|
+
bindingRevision,
|
|
209
|
+
batchKey,
|
|
210
|
+
sessionId,
|
|
211
|
+
sourceMessageIds: [...messageIds].slice(0, TASK_LIMITS.sourceMessages),
|
|
212
|
+
updatedAt: new Date(now).toISOString(),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function createEpisode(tasks, { taskKind, seriesId, phase, status, transitionId, previousEpisodeId, taskText, now }) {
|
|
217
|
+
const episodeId = episodeIdFor(transitionId);
|
|
218
|
+
const at = new Date(now).toISOString();
|
|
219
|
+
const episode = {
|
|
220
|
+
episodeId,
|
|
221
|
+
...(taskKind ? { taskKind } : {}),
|
|
222
|
+
...(seriesId ? { seriesId } : {}),
|
|
223
|
+
...(phase ? { phase } : {}),
|
|
224
|
+
status,
|
|
225
|
+
transitionId,
|
|
226
|
+
...(previousEpisodeId ? { previousEpisodeId } : {}),
|
|
227
|
+
...(text(taskText, 200) ? { taskText: text(taskText, 200) } : {}),
|
|
228
|
+
createdAt: at,
|
|
229
|
+
updatedAt: at,
|
|
230
|
+
};
|
|
231
|
+
tasks.episodes[episodeId] = episode;
|
|
232
|
+
tasks.revision += 1;
|
|
233
|
+
return episode;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function touch(tasks, episode, patch, now) {
|
|
237
|
+
const at = new Date(now).toISOString();
|
|
238
|
+
Object.assign(episode, patch, { updatedAt: at });
|
|
239
|
+
tasks.episodes[episode.episodeId] = episode;
|
|
240
|
+
tasks.revision += 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 依据可信批次与焦点决定本轮锚点。这里不读自由文本里的“可能栏目”,
|
|
245
|
+
* 只认明确切换、已确认延续、已确认项目默认和前台选择过的 proposed。
|
|
246
|
+
*/
|
|
247
|
+
export function routeTask(tasksInput, input = {}) {
|
|
248
|
+
const tasks = normalizeTasks(tasksInput);
|
|
249
|
+
const now = Number.isFinite(input.now) ? input.now : Date.now();
|
|
250
|
+
const sessionId = text(input.sessionId, 120);
|
|
251
|
+
const query = text(input.query, 400);
|
|
252
|
+
const messageIds = (Array.isArray(input.messageIds) ? input.messageIds : []).map((id) => text(id, 120)).filter(Boolean);
|
|
253
|
+
const batchKey = input.batchKey || requestBatchKey(messageIds);
|
|
254
|
+
const projectId = text(input.projectId, 240);
|
|
255
|
+
if (!sessionId || !batchKey) return { tasks, anchor: null, episode: null, decision: { kind: 'none', reason: 'no_batch' }, changed: false };
|
|
256
|
+
|
|
257
|
+
const current = tasks.sessions[sessionId] || null;
|
|
258
|
+
const currentEpisode = episodeOf(tasks, current && current.episodeId);
|
|
259
|
+
/** 同一批人类消息重试/重放:已有本轮结果就原样返回,不重复建 episode。 */
|
|
260
|
+
if (current && current.batchKey === batchKey) {
|
|
261
|
+
if (currentEpisode && currentEpisode.status !== 'unbound' && !input.selection) {
|
|
262
|
+
return { tasks, anchor: current, episode: currentEpisode, decision: { kind: 'focus', anchor: current }, changed: false };
|
|
263
|
+
}
|
|
264
|
+
if (currentEpisode && currentEpisode.status !== 'unbound' && input.selection
|
|
265
|
+
&& (!input.selection.seriesId || currentEpisode.seriesId === input.selection.seriesId)) {
|
|
266
|
+
return { tasks, anchor: current, episode: currentEpisode, decision: { kind: 'selected', anchor: current }, changed: false };
|
|
267
|
+
}
|
|
268
|
+
if (currentEpisode && currentEpisode.status === 'unbound' && !input.selection) {
|
|
269
|
+
return { tasks, anchor: current, episode: currentEpisode, decision: { kind: 'unbound', anchor: current }, changed: false };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const registered = (Array.isArray(input.registeredSeries) ? input.registeredSeries : []).map((item) => text(item, 40)).filter(Boolean);
|
|
273
|
+
/** 用户在这句话里点名的栏目优先于旧栏目和项目默认值。 */
|
|
274
|
+
const namedSeries = registered.find((series) => series && query.includes(series)) || '';
|
|
275
|
+
const changedSeries = current && namedSeries && namedSeries !== (current.seriesId || '') ? namedSeries : '';
|
|
276
|
+
const fallbackKind = (currentEpisode && currentEpisode.taskKind)
|
|
277
|
+
|| (input.defaults && input.defaults.taskKind) || '';
|
|
278
|
+
|
|
279
|
+
if ((!current || current.status === 'unbound' || current.status === 'proposed') && input.selection && (input.selection.seriesId || input.selection.taskKind)) {
|
|
280
|
+
const transitionId = transitionIdFor(batchKey, current ? current.bindingRevision || 0 : 0);
|
|
281
|
+
const existing = Object.values(tasks.episodes).find((episode) => episode.transitionId === transitionId);
|
|
282
|
+
/** 已有无正文作品壳时原地升级,不额外留一个作废的空集。 */
|
|
283
|
+
const shell = !existing && current && currentEpisode && currentEpisode.status === 'unbound' && !currentEpisode.seriesId && !currentEpisode.taskKind;
|
|
284
|
+
let episode;
|
|
285
|
+
if (shell) {
|
|
286
|
+
episode = currentEpisode;
|
|
287
|
+
Object.assign(episode, {
|
|
288
|
+
...(input.selection.taskKind ? { taskKind: input.selection.taskKind } : {}),
|
|
289
|
+
...(input.selection.seriesId ? { seriesId: input.selection.seriesId } : {}),
|
|
290
|
+
phase: input.selection.phase || 'brief',
|
|
291
|
+
});
|
|
292
|
+
touch(tasks, episode, { status: 'proposed' }, now);
|
|
293
|
+
} else {
|
|
294
|
+
episode = existing || createEpisode(tasks, {
|
|
295
|
+
taskKind: input.selection.taskKind,
|
|
296
|
+
seriesId: input.selection.seriesId,
|
|
297
|
+
phase: input.selection.phase || 'brief',
|
|
298
|
+
status: 'proposed',
|
|
299
|
+
transitionId,
|
|
300
|
+
taskText: query,
|
|
301
|
+
now,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const anchor = anchorOf(episode, { sessionId, batchKey, messageIds, status: 'proposed', bindingRevision: tasks.revision, now });
|
|
305
|
+
tasks.sessions[sessionId] = anchor;
|
|
306
|
+
return { tasks, anchor, episode, decision: { kind: 'selected', anchor }, changed: true };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (isThanks(query)) {
|
|
310
|
+
if (!current) return { tasks, anchor: null, episode: null, decision: { kind: 'none', reason: 'thanks' }, changed: false };
|
|
311
|
+
return { tasks, anchor: current, episode: currentEpisode, decision: { kind: 'none', reason: 'thanks', pointer: true }, changed: false };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** 先认新对象,再认续作:带「继续 / 这个」但换了对象,也必须撤销旧范围。 */
|
|
315
|
+
if (current && currentEpisode && currentEpisode.status !== 'unbound'
|
|
316
|
+
&& !SWITCH_WORDS.test(query) && !changedSeries
|
|
317
|
+
&& looksLikeNewTask(query, currentEpisode.taskText || '')) {
|
|
318
|
+
const transitionId = transitionIdFor(batchKey, tasks.revision);
|
|
319
|
+
const existing = Object.values(tasks.episodes).find((episode) => episode.transitionId === transitionId);
|
|
320
|
+
const episode = existing || createEpisode(tasks, {
|
|
321
|
+
status: 'unbound',
|
|
322
|
+
phase: 'brief',
|
|
323
|
+
transitionId,
|
|
324
|
+
previousEpisodeId: currentEpisode.episodeId,
|
|
325
|
+
taskText: query,
|
|
326
|
+
now,
|
|
327
|
+
});
|
|
328
|
+
const anchor = anchorOf(episode, { sessionId, batchKey, messageIds, status: 'unbound', bindingRevision: tasks.revision, now });
|
|
329
|
+
tasks.sessions[sessionId] = anchor;
|
|
330
|
+
return { tasks, anchor, episode, decision: { kind: 'unbound', anchor, reason: 'new_task' }, changed: true };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (isContinuation(query) && current && currentEpisode) {
|
|
334
|
+
const previousRevision = current.bindingRevision || 1;
|
|
335
|
+
const transitionId = transitionIdFor(batchKey, previousRevision);
|
|
336
|
+
const existing = Object.values(tasks.episodes).find((episode) => episode.transitionId === transitionId);
|
|
337
|
+
/** 「再做一期 Vlog」:继承上一期的任务类型,但栏目换成本句点名的那个。 */
|
|
338
|
+
const seriesId = changedSeries || currentEpisode.seriesId || '';
|
|
339
|
+
const status = changedSeries ? 'active' : 'continuable';
|
|
340
|
+
const episode = existing || createEpisode(tasks, {
|
|
341
|
+
...(fallbackKind ? { taskKind: fallbackKind } : {}),
|
|
342
|
+
...(seriesId ? { seriesId } : {}),
|
|
343
|
+
phase: 'brief',
|
|
344
|
+
status,
|
|
345
|
+
transitionId,
|
|
346
|
+
previousEpisodeId: currentEpisode.episodeId,
|
|
347
|
+
taskText: query,
|
|
348
|
+
now,
|
|
349
|
+
});
|
|
350
|
+
const anchor = anchorOf(episode, { sessionId, batchKey, messageIds, status, bindingRevision: tasks.revision, now });
|
|
351
|
+
tasks.sessions[sessionId] = anchor;
|
|
352
|
+
return { tasks, anchor, episode, decision: { kind: 'continuation', anchor, inherited: currentEpisode }, changed: true };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (isRevision(query) && current && currentEpisode) {
|
|
356
|
+
touch(tasks, currentEpisode, { status: currentEpisode.status === 'proposed' ? 'proposed' : 'continuable' }, now);
|
|
357
|
+
const anchor = anchorOf(currentEpisode, { sessionId, batchKey, messageIds, status: currentEpisode.status === 'proposed' ? 'proposed' : 'continuable', bindingRevision: tasks.revision, now });
|
|
358
|
+
tasks.sessions[sessionId] = anchor;
|
|
359
|
+
return { tasks, anchor, episode: currentEpisode, decision: { kind: 'revision', anchor }, changed: true };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (current && (SWITCH_WORDS.test(query) || changedSeries)) {
|
|
363
|
+
/** 明确点到已登记的栏目就切过去并立即生效;只说「换个」没点栏目才留无正文作品壳。 */
|
|
364
|
+
const nextSeries = changedSeries || '';
|
|
365
|
+
const transitionId = transitionIdFor(batchKey, tasks.revision);
|
|
366
|
+
const existing = Object.values(tasks.episodes).find((episode) => episode.transitionId === transitionId);
|
|
367
|
+
const status = nextSeries ? 'active' : 'unbound';
|
|
368
|
+
const episode = existing || createEpisode(tasks, {
|
|
369
|
+
...(fallbackKind ? { taskKind: fallbackKind } : {}),
|
|
370
|
+
...(nextSeries ? { seriesId: nextSeries } : {}),
|
|
371
|
+
status,
|
|
372
|
+
phase: 'brief',
|
|
373
|
+
transitionId,
|
|
374
|
+
previousEpisodeId: currentEpisode ? currentEpisode.episodeId : '',
|
|
375
|
+
taskText: query,
|
|
376
|
+
now,
|
|
377
|
+
});
|
|
378
|
+
const anchor = anchorOf(episode, { sessionId, batchKey, messageIds, status, bindingRevision: tasks.revision, now });
|
|
379
|
+
tasks.sessions[sessionId] = anchor;
|
|
380
|
+
return { tasks, anchor, episode, decision: { kind: nextSeries ? 'selected' : 'unbound', anchor, switched: true }, changed: true };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** 新会话或尚无有效栏目时,本句点名的已登记栏目直接生效,不被项目默认值覆盖。 */
|
|
384
|
+
if (namedSeries && !changedSeries && (!current || current.status === 'unbound' || current.status === 'proposed')) {
|
|
385
|
+
const transitionId = transitionIdFor(batchKey, tasks.revision);
|
|
386
|
+
const existing = Object.values(tasks.episodes).find((episode) => episode.transitionId === transitionId);
|
|
387
|
+
const episode = existing || createEpisode(tasks, {
|
|
388
|
+
...(fallbackKind ? { taskKind: fallbackKind } : {}),
|
|
389
|
+
seriesId: namedSeries,
|
|
390
|
+
status: 'active',
|
|
391
|
+
phase: 'brief',
|
|
392
|
+
transitionId,
|
|
393
|
+
taskText: query,
|
|
394
|
+
now,
|
|
395
|
+
});
|
|
396
|
+
const anchor = anchorOf(episode, { sessionId, batchKey, messageIds, status: 'active', bindingRevision: tasks.revision, now });
|
|
397
|
+
tasks.sessions[sessionId] = anchor;
|
|
398
|
+
return { tasks, anchor, episode, decision: { kind: 'selected', anchor, explicit: true }, changed: true };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (current && currentEpisode) {
|
|
402
|
+
/** 任务文本只作后续「是否换任务」的比对材料,不改锚点时间,免得把正在整理的这期作废。 */
|
|
403
|
+
const nextTaskText = text(query, 200);
|
|
404
|
+
if (nextTaskText && currentEpisode.taskText !== nextTaskText) {
|
|
405
|
+
currentEpisode.taskText = nextTaskText;
|
|
406
|
+
tasks.episodes[currentEpisode.episodeId] = currentEpisode;
|
|
407
|
+
}
|
|
408
|
+
const anchor = anchorOf(currentEpisode, {
|
|
409
|
+
sessionId, batchKey, messageIds,
|
|
410
|
+
status: currentEpisode.status,
|
|
411
|
+
bindingRevision: current.bindingRevision || tasks.revision,
|
|
412
|
+
now,
|
|
413
|
+
});
|
|
414
|
+
tasks.sessions[sessionId] = anchor;
|
|
415
|
+
return { tasks, anchor, episode: currentEpisode, decision: { kind: 'focus', anchor }, changed: false };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const defaults = input.defaults || tasks.defaults;
|
|
419
|
+
if (defaults && (defaults.seriesId || defaults.taskKind)) {
|
|
420
|
+
const transitionId = transitionIdFor(batchKey, tasks.revision);
|
|
421
|
+
const existing = Object.values(tasks.episodes).find((episode) => episode.transitionId === transitionId);
|
|
422
|
+
const episode = existing || createEpisode(tasks, {
|
|
423
|
+
taskKind: defaults.taskKind,
|
|
424
|
+
seriesId: defaults.seriesId,
|
|
425
|
+
phase: 'brief',
|
|
426
|
+
status: 'active',
|
|
427
|
+
transitionId,
|
|
428
|
+
taskText: query,
|
|
429
|
+
now,
|
|
430
|
+
});
|
|
431
|
+
const anchor = anchorOf(episode, { sessionId, batchKey, messageIds, status: 'active', bindingRevision: tasks.revision, now });
|
|
432
|
+
tasks.sessions[sessionId] = anchor;
|
|
433
|
+
return { tasks, anchor, episode, decision: { kind: 'default', anchor }, changed: true };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** 新会话/新任务、无法确定栏目:只建无正文作品壳,不猜 taskKind/seriesId。 */
|
|
437
|
+
const transitionId = transitionIdFor(batchKey, tasks.revision);
|
|
438
|
+
const existing = Object.values(tasks.episodes).find((episode) => episode.transitionId === transitionId);
|
|
439
|
+
const episode = existing || createEpisode(tasks, { status: 'unbound', phase: 'brief', transitionId, taskText: query, now });
|
|
440
|
+
const anchor = anchorOf(episode, { sessionId, batchKey, messageIds, status: 'unbound', bindingRevision: tasks.revision, now });
|
|
441
|
+
tasks.sessions[sessionId] = anchor;
|
|
442
|
+
return { tasks, anchor, episode, decision: { kind: 'unbound', anchor }, changed: true };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function contextOfAnchor(anchor, base = {}) {
|
|
446
|
+
return {
|
|
447
|
+
...base,
|
|
448
|
+
...(anchor && anchor.taskKind ? { taskKind: anchor.taskKind } : {}),
|
|
449
|
+
...(anchor && anchor.seriesId ? { seriesId: anchor.seriesId } : {}),
|
|
450
|
+
...(anchor && anchor.phase ? { phase: anchor.phase } : {}),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** 自动读取决策表:只有明确绑定才给正文;未绑定只给栏目索引。 */
|
|
455
|
+
export function decideRead(input = {}) {
|
|
456
|
+
if (input.mode === 'off') return { kind: 'none', reason: 'mode_off' };
|
|
457
|
+
if (input.enabled !== true) return { kind: 'none', reason: 'experiment_off' };
|
|
458
|
+
if (input.identityConfirmed !== true) return { kind: 'none', reason: 'identity_unknown' };
|
|
459
|
+
if (input.thanks) return { kind: 'none', reason: 'thanks', pointerOnly: true };
|
|
460
|
+
if (input.conflict) return { kind: 'conflict', reason: 'conflict' };
|
|
461
|
+
const anchor = input.anchor;
|
|
462
|
+
if (!anchor || anchor.status === 'unbound') {
|
|
463
|
+
const ranges = (Array.isArray(input.ranges) ? input.ranges : []).filter(Boolean).slice(0, 3);
|
|
464
|
+
return ranges.length
|
|
465
|
+
? { kind: 'index', ranges, reason: 'unbound' }
|
|
466
|
+
: { kind: 'none', reason: 'no_range' };
|
|
467
|
+
}
|
|
468
|
+
if (anchor.status === 'ambiguous') {
|
|
469
|
+
const ranges = (Array.isArray(input.ranges) ? input.ranges : []).filter(Boolean).slice(0, 3);
|
|
470
|
+
return { kind: 'index', ranges, reason: 'ambiguous' };
|
|
471
|
+
}
|
|
472
|
+
if (anchor.status === 'proposed' && input.boundForRequest !== true) {
|
|
473
|
+
return { kind: 'index', ranges: (Array.isArray(input.ranges) ? input.ranges : []).filter(Boolean).slice(0, 3), reason: 'proposed' };
|
|
474
|
+
}
|
|
475
|
+
return { kind: 'rules', reason: 'bound' };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function applySelection(tasksInput, { sessionId, selection, batchKey, messageIds, now = Date.now() }) {
|
|
479
|
+
return routeTask(tasksInput, {
|
|
480
|
+
sessionId,
|
|
481
|
+
batchKey,
|
|
482
|
+
messageIds,
|
|
483
|
+
query: '',
|
|
484
|
+
selection,
|
|
485
|
+
now,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function setDefault(tasksInput, input = {}) {
|
|
490
|
+
const tasks = normalizeTasks(tasksInput);
|
|
491
|
+
const taskKind = text(input.taskKind, 40);
|
|
492
|
+
const seriesId = text(input.seriesId, 40);
|
|
493
|
+
const phase = text(input.phase, 40);
|
|
494
|
+
if (!taskKind && !seriesId && !phase) {
|
|
495
|
+
tasks.defaults = null;
|
|
496
|
+
tasks.revision += 1;
|
|
497
|
+
return tasks;
|
|
498
|
+
}
|
|
499
|
+
tasks.defaults = {
|
|
500
|
+
...(taskKind ? { taskKind } : {}),
|
|
501
|
+
...(seriesId ? { seriesId } : {}),
|
|
502
|
+
...(phase ? { phase } : {}),
|
|
503
|
+
confirmedBy: 'user',
|
|
504
|
+
confirmedAt: new Date(input.now || Date.now()).toISOString(),
|
|
505
|
+
};
|
|
506
|
+
tasks.revision += 1;
|
|
507
|
+
return tasks;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function closeSession(tasksInput, sessionId) {
|
|
511
|
+
const tasks = normalizeTasks(tasksInput);
|
|
512
|
+
const anchor = tasks.sessions[String(sessionId || '')];
|
|
513
|
+
if (!anchor) return { tasks, changed: false };
|
|
514
|
+
anchor.status = 'closed';
|
|
515
|
+
anchor.updatedAt = new Date().toISOString();
|
|
516
|
+
tasks.revision += 1;
|
|
517
|
+
return { tasks, changed: true };
|
|
518
|
+
}
|
package/bench/bench.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrieval-path benchmark on a fixed 200-project + 200-global dataset.
|
|
3
|
+
* Local evidence only: prints one JSON object; no cloud model, no network.
|
|
4
|
+
*
|
|
5
|
+
* node bench/bench.mjs
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { EventEmitter } from 'node:events';
|
|
11
|
+
|
|
12
|
+
process.env.DSH_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-bench-'));
|
|
13
|
+
const { apply } = await import('../index.js');
|
|
14
|
+
const store = await import('../store.js');
|
|
15
|
+
|
|
16
|
+
const ORIGIN = 'http://127.0.0.1:60403';
|
|
17
|
+
const defs = new Map();
|
|
18
|
+
const subscriptions = new Map();
|
|
19
|
+
const promptContexts = [];
|
|
20
|
+
let currentAgent = null;
|
|
21
|
+
const webServer = { getOrigin: () => ORIGIN, register: () => () => {} };
|
|
22
|
+
const effect = (fn) => {
|
|
23
|
+
const dispose = fn();
|
|
24
|
+
return typeof dispose === 'function' ? dispose : () => {};
|
|
25
|
+
};
|
|
26
|
+
const ctx = {
|
|
27
|
+
tools: { register: (def) => { defs.set(def.name, def); return () => {}; } },
|
|
28
|
+
effect,
|
|
29
|
+
inject: (deps, callback) => { if (deps.includes('webServer')) callback({ webServer, effect }); return () => {}; },
|
|
30
|
+
get: (name) => {
|
|
31
|
+
if (name === 'systemPrompt') return { context: (entry) => { promptContexts.push(entry); return () => {}; } };
|
|
32
|
+
if (name === 'agents') return { currentInitiator: () => currentAgent };
|
|
33
|
+
return undefined;
|
|
34
|
+
},
|
|
35
|
+
on: (event, handler) => { subscriptions.set(event, handler); return () => {}; },
|
|
36
|
+
};
|
|
37
|
+
apply(ctx);
|
|
38
|
+
|
|
39
|
+
const cwd = path.join(os.tmpdir(), 'lite-memory-bench-project');
|
|
40
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
41
|
+
const agent = { cwd, session: { id: 'bench-session', header: { cwd } } };
|
|
42
|
+
const exec = (name, args) => defs.get(name).execute(args, { agent });
|
|
43
|
+
const save = (args) => exec('memory_save', args);
|
|
44
|
+
|
|
45
|
+
const pct = (values, p) => {
|
|
46
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
47
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))];
|
|
48
|
+
};
|
|
49
|
+
const rss = () => Math.round(process.memoryUsage().rss / (1024 * 1024) * 10) / 10;
|
|
50
|
+
|
|
51
|
+
const report = { node: process.version, platform: `${process.platform}/${process.arch}`, limits: store.LIMITS };
|
|
52
|
+
const started = Date.now();
|
|
53
|
+
const seedAgent = { ...agent };
|
|
54
|
+
|
|
55
|
+
// 200 project entries and 200 global entries, all with the same shape.
|
|
56
|
+
for (let i = 0; i < 200; i += 1) {
|
|
57
|
+
save({
|
|
58
|
+
title: `项目条目 ${i} 发布顺序`,
|
|
59
|
+
summary: `项目条目 ${i} 的前置说明`,
|
|
60
|
+
when: `项目场景 ${i}`,
|
|
61
|
+
body: `项目正文 ${i}:先跑测试,再发布。`,
|
|
62
|
+
applicability: 'all_in_scope',
|
|
63
|
+
resolution: 'distinct',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
for (let i = 0; i < 200; i += 1) {
|
|
67
|
+
save({
|
|
68
|
+
title: `全局条目 ${i} 交付规范`,
|
|
69
|
+
summary: `全局条目 ${i} 的通用说明`,
|
|
70
|
+
when: `全局场景 ${i}`,
|
|
71
|
+
body: `全局正文 ${i}:交付前清零告警。`,
|
|
72
|
+
scope: 'global',
|
|
73
|
+
applicability: 'all_in_scope',
|
|
74
|
+
resolution: 'distinct',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
report.seedMs = Date.now() - started;
|
|
78
|
+
report.entries = {
|
|
79
|
+
project: store.loadIndex(store.scopeOf(cwd)).entries.length,
|
|
80
|
+
global: store.loadIndex(store.globalScope()).entries.length,
|
|
81
|
+
};
|
|
82
|
+
report.rssAfterSeedMB = rss();
|
|
83
|
+
|
|
84
|
+
// Hot retrieval path: pre-step prepare + prompt context read, 120 runs.
|
|
85
|
+
const preStep = subscriptions.get('agent/pre-step');
|
|
86
|
+
if (typeof preStep !== 'function') throw new Error('pre-step handler missing');
|
|
87
|
+
const recall = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
|
|
88
|
+
if (!recall) throw new Error('recall context missing');
|
|
89
|
+
const userMessage = (i) => async () => ({
|
|
90
|
+
kind: 'enter',
|
|
91
|
+
messages: [{ role: 'user', id: `bench-h${i}`, source: { kind: 'user' }, content: [{ type: 'text', text: `项目条目 ${i % 200} 的发布顺序是什么,第 ${i} 次问` }] }],
|
|
92
|
+
});
|
|
93
|
+
const timings = [];
|
|
94
|
+
let bytes = 0;
|
|
95
|
+
for (let i = 0; i < 120; i += 1) {
|
|
96
|
+
const signal = new AbortController().signal;
|
|
97
|
+
const t = process.hrtime.bigint();
|
|
98
|
+
await preStep({ agent, signal, turn: String(i + 2) }, userMessage(i));
|
|
99
|
+
bytes += Buffer.byteLength(recall.text({ agent, signal }), 'utf8');
|
|
100
|
+
timings.push(Number(process.hrtime.bigint() - t) / 1e6);
|
|
101
|
+
}
|
|
102
|
+
report.prepare = {
|
|
103
|
+
runs: timings.length,
|
|
104
|
+
coldMs: Math.round(timings[0] * 10) / 10,
|
|
105
|
+
p50Ms: Math.round(pct(timings, 50) * 10) / 10,
|
|
106
|
+
p95Ms: Math.round(pct(timings, 95) * 10) / 10,
|
|
107
|
+
maxMs: Math.round(Math.max(...timings) * 10) / 10,
|
|
108
|
+
avgContextBytes: Math.round(bytes / timings.length),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// File reads per prepare, measured on the same path.
|
|
112
|
+
let reads = 0;
|
|
113
|
+
const original = fs.readFileSync;
|
|
114
|
+
fs.readFileSync = function counted(...args) {
|
|
115
|
+
reads += 1;
|
|
116
|
+
return original.apply(this, args);
|
|
117
|
+
};
|
|
118
|
+
const before = reads;
|
|
119
|
+
for (let i = 0; i < 40; i += 1) {
|
|
120
|
+
await preStep({ agent, signal: new AbortController().signal, turn: String(200 + i) }, userMessage(i));
|
|
121
|
+
}
|
|
122
|
+
fs.readFileSync = original;
|
|
123
|
+
report.fileReadsPerPrepare = Math.round(((reads - before) / 40) * 100) / 100;
|
|
124
|
+
|
|
125
|
+
// Long sequence: save / read / search / forget, 150 rounds.
|
|
126
|
+
const seqStart = Date.now();
|
|
127
|
+
for (let i = 0; i < 150; i += 1) {
|
|
128
|
+
const saved = save({ title: `序列条目 ${i}`, summary: `序列 ${i} 的摘要`, applicability: 'all_in_scope', resolution: 'distinct' });
|
|
129
|
+
if (saved && saved.id) exec('memory_read', { id: saved.id });
|
|
130
|
+
exec('memory_search', { query: `序列条目 ${i}` });
|
|
131
|
+
if (i % 3 === 0 && saved && saved.id) exec('memory_forget', { id: saved.id });
|
|
132
|
+
}
|
|
133
|
+
report.sequenceMs = Date.now() - seqStart;
|
|
134
|
+
const project = store.loadIndex(store.scopeOf(cwd));
|
|
135
|
+
report.afterSequence = {
|
|
136
|
+
projectActive: project.entries.length,
|
|
137
|
+
projectArchived: project.archived.length,
|
|
138
|
+
globalActive: store.loadIndex(store.globalScope()).entries.length,
|
|
139
|
+
stateBytes: fs.statSync(path.join(store.scopeOf(cwd).dir, 'state.json')).size,
|
|
140
|
+
files: fs.readdirSync(store.scopeOf(cwd).dir).sort(),
|
|
141
|
+
};
|
|
142
|
+
report.rssEndMB = rss();
|
|
143
|
+
report.ok = project.entries.length <= store.LIMITS.entries + store.LIMITS.hardEntries;
|
|
144
|
+
|
|
145
|
+
console.log(JSON.stringify(report, null, 2));
|