@kira905/dsh-memory-toolkit 0.1.3 → 0.2.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.en.md +225 -2
  3. package/README.md +180 -2
  4. package/lib/deploy-guard.mjs +463 -275
  5. package/lib/guard-http.mjs +59 -62
  6. package/lib/pitfall-form-map.mjs +94 -84
  7. package/lib/pitfall-score.mjs +262 -185
  8. package/lib/session-file-name.mjs +116 -100
  9. package/lib/zstd-frames.mjs +88 -67
  10. package/memory-db-tools/README.md +29 -1
  11. package/memory-db-tools/audit-analyze.mjs +1 -1
  12. package/memory-db-tools/audit-export.mjs +60 -16
  13. package/memory-db-tools/audit-struct-probe.mjs +29 -8
  14. package/memory-db-tools/lib/known-devices.mjs +137 -0
  15. package/memory-engine/README.md +58 -4
  16. package/memory-engine/bench/bench.mjs +1 -1
  17. package/memory-engine/cli/band-sentinel.mjs +164 -0
  18. package/memory-engine/cli/distill-rollback.mjs +112 -44
  19. package/memory-engine/cli/distill-sessions.mjs +237 -0
  20. package/memory-engine/cli/distill.mjs +196 -95
  21. package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
  22. package/memory-engine/cli/doc-index.mjs +47 -38
  23. package/memory-engine/cli/domain-tags.mjs +246 -162
  24. package/memory-engine/cli/layer-migrate.mjs +176 -0
  25. package/memory-engine/cli/layer-move.mjs +189 -0
  26. package/memory-engine/cli/pitfall-hook.mjs +136 -70
  27. package/memory-engine/cli/recall.mjs +243 -110
  28. package/memory-engine/cli/selfcheck.mjs +93 -44
  29. package/memory-engine/cli/serve-start.mjs +221 -140
  30. package/memory-engine/cli/sweep.mjs +83 -44
  31. package/memory-engine/cli/sync.mjs +93 -57
  32. package/memory-engine/config/audit-devices.json +6 -0
  33. package/memory-engine/config/bands.json +7 -0
  34. package/memory-engine/config/layers.json +7 -0
  35. package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
  36. package/memory-engine/dict/lexicon-config.json +11 -0
  37. package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
  38. package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
  39. package/memory-engine/lib/band-sentinel.mjs +417 -0
  40. package/memory-engine/lib/cli-strict.mjs +128 -0
  41. package/memory-engine/lib/cli-util.mjs +147 -125
  42. package/memory-engine/lib/config.mjs +83 -96
  43. package/memory-engine/lib/dirty-ledger.mjs +127 -94
  44. package/memory-engine/lib/distill-rollback.mjs +173 -133
  45. package/memory-engine/lib/distill.mjs +878 -585
  46. package/memory-engine/lib/doc-index-store.mjs +208 -235
  47. package/memory-engine/lib/doc-index.mjs +351 -350
  48. package/memory-engine/lib/doc-scan.mjs +178 -156
  49. package/memory-engine/lib/doc-summarize.mjs +220 -205
  50. package/memory-engine/lib/domain-tags.mjs +358 -277
  51. package/memory-engine/lib/embedder.mjs +175 -194
  52. package/memory-engine/lib/hashes.mjs +22 -7
  53. package/memory-engine/lib/layer-migrate.mjs +330 -0
  54. package/memory-engine/lib/layer-move.mjs +307 -0
  55. package/memory-engine/lib/layers.mjs +191 -0
  56. package/memory-engine/lib/lexicon-recall.mjs +473 -0
  57. package/memory-engine/lib/memory-source.mjs +78 -80
  58. package/memory-engine/lib/pitfall-source.mjs +115 -114
  59. package/memory-engine/lib/policy-source.mjs +279 -261
  60. package/memory-engine/lib/recall-log.mjs +285 -0
  61. package/memory-engine/lib/recall.mjs +565 -240
  62. package/memory-engine/lib/selfcheck.mjs +142 -106
  63. package/memory-engine/lib/session-distill.mjs +603 -0
  64. package/memory-engine/lib/session-source.mjs +421 -0
  65. package/memory-engine/lib/sweep.mjs +107 -130
  66. package/memory-engine/lib/sync.mjs +110 -140
  67. package/memory-engine/lib/tick-hook.mjs +123 -148
  68. package/memory-engine/lib/vectors-store.mjs +213 -188
  69. package/memory-engine/test/12-layering.test.mjs +539 -0
  70. package/memory-engine/test/13-session-distill.test.mjs +616 -0
  71. package/memory-engine/tools/bridge-smoke.mjs +94 -55
  72. package/memory-engine/tools/fetch-model.mjs +167 -87
  73. package/memory-engine/tools/smoke-recall.mjs +94 -38
  74. package/memory-engine/tools/verify-recall-chain.mjs +413 -0
  75. package/package.json +1 -1
@@ -1,218 +1,193 @@
1
- // 记忆引擎 v2 · 离线层 —— 坑库向量同步「标记 → 消费」钩子(方案 §A.2 第 10 行 · A 点)
1
+ // 坑库向量同步的「置标记 → 消费」钩子(方案 §A.2 第 10 行 · A 点)
2
2
  //
3
- // 要解决的问题:坑库(butler-pitfalls.json)变了之后,谁去触发 `source='pitfall'` 的增量编码?
4
- // 方案选的是**松耦合信号**:`pitfall` job 末尾**只置一个标记**(不直接编码、不拖慢 job),
5
- // 消费方(本模块,由 `memory-vectors` 档或手动 CLI 拉起)看到标记才跑一次坑源增量同步。
3
+ // 要解决的问题:坑库文件变了以后,谁去触发 `source='pitfall'` 的增量编码?
4
+ // 这里选的是**松耦合信号**:坑库 job 跑完只落一个标记(不直接编码、不拖慢 job),
5
+ // 消费方(本模块)看到标记才跑一次坑源增量同步。
6
6
  //
7
- // ⚠️ 这是全方案**唯一"有意双写者"**的文件(方案 §A.4 · C5 2026-10-08 回修):
8
- // ① 置标记方 = `pitfall` job ── 原子写(临时文件 + rename),**必须写 `updatedAt`**
9
- // ② 清标记方 = 本模块消费者 ── **原子读改写**,且**只在标记 `updatedAt` 早于本次消费起点时才清**
10
- // (否则会把并发新写入的标记一起清掉 = 丢标记、下轮不编码、还无人告警)
11
- // 两方**不共用同一把锁**(跨 job 共用会把握手串成串行);靠 `updatedAt` stale 判定 + 原子 rename 兜住。
7
+ // ⚠ 全方案唯一"有意双写者"的一处:置标记方与清标记方各写各的,**不共用同一把锁**
8
+ // (跨进程共用锁会把握手串成串行)。正确性靠三件事兜住:
9
+ // ① 置标记方:临时文件 + rename 原子落盘,且**必写 `updatedAt`**;
10
+ // ② 清标记方:原子读改写,且**只在标记 `updatedAt` 早于本次消费起点时才清**;
11
+ // ③ 并发插进新标记时宁可这轮不清(留待下次),绝不丢标记。
12
12
  import fs from 'node:fs';
13
13
  import os from 'node:os';
14
14
  import path from 'node:path';
15
15
  import { runIncrementalSync } from './sync.mjs';
16
16
 
17
17
  export const MARKER_SCHEMA = 1;
18
- /** 标记文件默认名(登记见 `tools/dsh-butler/lib/state-files.mjs`) */
18
+ // 标记文件默认名(已登记在管家的状态文件清单里)
19
19
  export const MARKER_FILENAME = 'butler-pitfall-vector-dirty.json';
20
20
 
21
- /** 默认标记文件路径(DSH_HOME 口径;可注入) */
21
+ // 默认落点:<DSH_HOME>/<标记文件名>(可注入,方便夹具与 CLI 指到别处)
22
22
  export function markerPathFor(dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')) {
23
23
  return path.join(dshHome, MARKER_FILENAME);
24
24
  }
25
25
 
26
- function tmpName(p) {
27
- return `${p}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
26
+ // Windows 上覆盖一个正被别人读/改名的目标会概率性抛这几个码(没有 flock 语义,只能重试)
27
+ const RENAME_RACE_CODES = new Set(['EPERM', 'EBUSY', 'EACCES', 'EEXIST']);
28
+
29
+ // 同步 API 里没法 await:只能空转,毫秒级,代价可忽略
30
+ function sleepSync(ms) {
31
+ const deadline = Date.now() + ms;
32
+ while (Date.now() < deadline) { /* 空转 */ }
28
33
  }
29
34
 
30
- /**
31
- * Windows 上 `rename` 覆盖一个**正被另一个进程读/改名**的目标会概率性抛
32
- * `EPERM/EBUSY/EACCES/EEXIST`(无 flock 语义)⇒ 有限次重试(毫秒级,不改变原子性:
33
- * 仍是"临时文件 → rename"两步,读者永远看不到半截内容)。
34
- */
35
- function renameWithRetry(from, to, { tries = 50, waitMs = 2 } = {}) {
36
- let lastErr = null;
37
- for (let i = 0; i < tries; i += 1) {
35
+ // 有限次重试的 rename。原子性不变:仍是"临时文件 → rename"两步,
36
+ // 读者要么看到旧内容、要么看到新内容,永远看不到半截。
37
+ function renameSyncWithRetry(from, to, { tries = 50, waitMs = 2 } = {}) {
38
+ let lastError = null;
39
+ for (let attempt = 0; attempt < tries; attempt += 1) {
38
40
  try {
39
41
  fs.renameSync(from, to);
40
42
  return true;
41
- } catch (e) {
42
- lastErr = e;
43
- if (!['EPERM', 'EBUSY', 'EACCES', 'EEXIST'].includes(e && e.code)) throw e;
44
- const until = Date.now() + waitMs;
45
- while (Date.now() < until) {
46
- /* 忙等几毫秒(同步 API,无法 await) */
47
- }
43
+ } catch (err) {
44
+ lastError = err;
45
+ if (!RENAME_RACE_CODES.has(err && err.code)) throw err;
46
+ sleepSync(waitMs);
48
47
  }
49
48
  }
50
- throw lastErr;
49
+ throw lastError;
50
+ }
51
+
52
+ // 标记体公共前缀:所有落盘形状都从这里长出来,字段顺序保持一致
53
+ function stampPayload(fields) {
54
+ return { schemaVersion: MARKER_SCHEMA, ...fields };
51
55
  }
52
56
 
53
- /** 原子写 JSON(临时文件 + 同卷 rename;读者永远不会看到半截 JSON) */
57
+ // 原子写 JSON(临时文件 + 同卷 rename;读者不会看到半截 JSON)
54
58
  export function atomicWriteJson(p, obj) {
55
59
  fs.mkdirSync(path.dirname(p), { recursive: true });
56
- const tmp = tmpName(p);
57
- fs.writeFileSync(tmp, `${JSON.stringify(obj, null, 2)}\n`, 'utf8');
58
- renameWithRetry(tmp, p);
60
+ const staged = `${p}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
61
+ fs.writeFileSync(staged, `${JSON.stringify(obj, null, 2)}\n`, 'utf8');
62
+ renameSyncWithRetry(staged, p);
59
63
  return p;
60
64
  }
61
65
 
62
- /**
63
- * 读标记。**永不抛错**(半截 JSON / 权限问题 ⇒ 返回 error,由调用方决定 fail-open 还是 fail-closed)。
64
- * @returns {{present:boolean, marker:object|null, error:string|null}}
65
- */
66
+ // 统一的"没读到"形状(三处出口共用,字段顺序稳定)
67
+ function absent(error) {
68
+ return { present: false, marker: null, error };
69
+ }
70
+
71
+ // 读标记。**永不抛错**:半截 JSON / 权限问题一律转成 error,由调用方决定 fail-open 还是 fail-closed。
66
72
  export function readMarker(p) {
67
73
  let raw;
68
74
  try {
69
75
  raw = fs.readFileSync(p, 'utf8');
70
- } catch (e) {
71
- if (e && e.code === 'ENOENT') return { present: false, marker: null, error: null };
72
- return { present: false, marker: null, error: String(e && e.message ? e.message : e) };
76
+ } catch (err) {
77
+ if (err && err.code === 'ENOENT') return absent(null);
78
+ return absent(String(err && err.message ? err.message : err));
73
79
  }
74
80
  try {
75
81
  return { present: true, marker: JSON.parse(raw), error: null };
76
- } catch (e) {
77
- return { present: false, marker: null, error: `半截/非法 JSON:${String(e && e.message ? e.message : e)}` };
82
+ } catch (err) {
83
+ return absent(`半截/非法 JSON:${String(err && err.message ? err.message : err)}`);
78
84
  }
79
85
  }
80
86
 
81
- /**
82
- * 置标记(**置标记方 API**;`pitfall` job 末尾一行调用)。
83
- * @param {string} p 标记文件路径
84
- * @param {{at?:number, reason?:string, by?:string, counters?:object}} [opts] `at` 必写(= 本次置标记时刻)
85
- */
87
+ // 置标记(置标记方 API,坑库 job 末尾一行调用);`at` 必写 = 本次置标记时刻
86
88
  export function markDirty(p, opts = {}) {
87
89
  const at = Number(opts.at ?? Date.now());
88
- atomicWriteJson(p, {
89
- schemaVersion: MARKER_SCHEMA,
90
+ const fields = {
90
91
  updatedAt: at,
91
92
  at: new Date(at).toISOString(),
92
93
  reason: opts.reason ?? null,
93
94
  by: opts.by ?? null,
94
95
  device: os.hostname(),
95
96
  counters: opts.counters ?? null,
96
- });
97
+ };
98
+ atomicWriteJson(p, stampPayload(fields));
97
99
  return { ok: true, path: p, updatedAt: at };
98
100
  }
99
101
 
102
+ // 删中间痕迹(失败也不影响判定结果)
103
+ function dropQuietly(file) {
104
+ try { fs.rmSync(file, { force: true }); } catch { /* 忽略 */ }
105
+ }
106
+
100
107
  /**
101
- * 清标记(**消费方 API**):原子读改写 + stale 判定。
108
+ * 清标记(消费方 API):原子读改写 + stale 判定。
102
109
  * · 标记缺失 ⇒ 什么都不做
103
- * · `updatedAt >= consumeStart` ⇒ **保留**(并发新标记,不是这次要消费的那份)
104
- * · `updatedAt < consumeStart` ⇒ 摘除(先写 `consumed` 落点,再 `rename` 摘除主文件;同卷 rename 是原子的)
105
- * 残余竞态窗:`复读` 与 `rename` 之间仍有极小窗口(Windows 无 flock 的文件级替代);
106
- * 实跑频率差 ≥ 3 个数量级(生产置标记 30 分钟一次 / 消费秒级),实际不构成丢标记。
107
- * @returns {{cleared:boolean, reason:string, consumedUpdatedAt?:number, marker?:object}}
110
+ * · updatedAt >= consumeStart ⇒ 保留(那是并发新写的,不是这轮要消费的那份)
111
+ * · updatedAt < consumeStart ⇒ 摘除:先写 consumed 落点,再把主文件 rename 过去(同卷 rename 原子)
112
+ * 残余竞态窗:复读与 rename 之间仍有极小窗口(Windows 上没有 flock 可替代);
113
+ * 两边实际频率差三个数量级以上(生产置标记 30 分钟一次 / 消费秒级),实际不构成丢标记。
108
114
  */
109
115
  export function clearMarkerIfStale(p, consumeStart) {
110
- const first = readMarker(p);
111
- if (!first.present) return { cleared: false, reason: first.error ? 'unreadable' : 'no-marker' };
112
- const at = Number(first.marker?.updatedAt || 0);
113
- if (!(at < Number(consumeStart))) return { cleared: false, reason: 'fresh-marker-kept', marker: first.marker };
114
-
115
- const consumed = `${p}.consumed`;
116
- atomicWriteJson(consumed, {
117
- schemaVersion: MARKER_SCHEMA,
118
- clearedAt: Date.now(),
119
- consumedUpdatedAt: at,
120
- });
116
+ const head = readMarker(p);
117
+ if (!head.present) return { cleared: false, reason: head.error ? 'unreadable' : 'no-marker' };
121
118
 
122
- const second = readMarker(p);
123
- if (!second.present) {
124
- try {
125
- fs.rmSync(consumed, { force: true });
126
- } catch {
127
- /* ignore */
128
- }
119
+ const seenAt = Number(head.marker?.updatedAt || 0);
120
+ if (!(seenAt < Number(consumeStart))) {
121
+ return { cleared: false, reason: 'fresh-marker-kept', marker: head.marker };
122
+ }
123
+
124
+ const consumedFile = `${p}.consumed`;
125
+ atomicWriteJson(consumedFile, stampPayload({ clearedAt: Date.now(), consumedUpdatedAt: seenAt }));
126
+
127
+ // 复读一次:这中间可能有人又置了新标记
128
+ const recheck = readMarker(p);
129
+ if (!recheck.present) {
130
+ dropQuietly(consumedFile);
129
131
  return { cleared: false, reason: 'already-gone' };
130
132
  }
131
- if (!(Number(second.marker?.updatedAt || 0) < Number(consumeStart))) {
132
- try {
133
- fs.rmSync(consumed, { force: true });
134
- } catch {
135
- /* ignore */
136
- }
137
- return { cleared: false, reason: 'fresh-marker-kept-on-recheck', marker: second.marker };
133
+ if (!(Number(recheck.marker?.updatedAt || 0) < Number(consumeStart))) {
134
+ dropQuietly(consumedFile);
135
+ return { cleared: false, reason: 'fresh-marker-kept-on-recheck', marker: recheck.marker };
138
136
  }
139
137
  try {
140
- renameWithRetry(p, consumed);
141
- } catch (e) {
142
- try {
143
- fs.rmSync(consumed, { force: true });
144
- } catch {
145
- /* ignore */
146
- }
147
- return { cleared: false, reason: `rename-failed:${e && e.code ? e.code : String(e)}` };
138
+ renameSyncWithRetry(p, consumedFile);
139
+ } catch (err) {
140
+ dropQuietly(consumedFile);
141
+ return { cleared: false, reason: `rename-failed:${err && err.code ? err.code : String(err)}` };
148
142
  }
149
- try {
150
- fs.rmSync(consumed, { force: true });
151
- } catch {
152
- /* ignore */
143
+ dropQuietly(consumedFile);
144
+ return { cleared: true, reason: 'stale-marker-consumed', consumedUpdatedAt: seenAt };
145
+ }
146
+
147
+ // 来源守卫:只认坑源。若被别的来源调进来,本函数会一边按 pitfall 命名空间写向量、
148
+ // 一边扫别人的行 ⇒ 静默污染命名空间。所以来源名不对就**拒跑**(宁可少跑一轮,也不脏库)。
149
+ function assertPitfallSource(source) {
150
+ const name = source && source.sourceName ? String(source.sourceName) : null;
151
+ if (name === 'pitfall') return name;
152
+ throw new Error(
153
+ `runPitfallHook 只接受坑源(sourceName='pitfall'),收到 ${name ?? '(未知来源/无 sourceName)'}`
154
+ + ' ⇒ 拒跑(否则会把该来源的行错标成 pitfall 向量)',
155
+ );
156
+ }
157
+
158
+ // 按本轮结果决定标记去留:dry-run 不动;同步失败保留;成功才清
159
+ function settleMarker({ markerPath, consumeStart, dryRun, stats, log }) {
160
+ if (dryRun) return { cleared: false, reason: 'dry-run' };
161
+ if (stats.error) {
162
+ log(`[tick-hook] 同步失败 ⇒ **保留标记**下轮重试:${stats.error}`);
163
+ return { cleared: false, reason: 'sync-failed-keep-marker' };
153
164
  }
154
- return { cleared: true, reason: 'stale-marker-consumed', consumedUpdatedAt: at };
165
+ const outcome = clearMarkerIfStale(markerPath, consumeStart);
166
+ log(`[tick-hook] 清标记:${JSON.stringify(outcome)}`);
167
+ return outcome;
155
168
  }
156
169
 
157
- /**
158
- * 消费一轮:有标记 ⇒ 跑坑源增量同步 ⇒ 成功后清标记(失败**不清**,下轮重试)。
159
- * @param {object} p
160
- * @param {string} p.markerPath
161
- * @param {object} p.store / p.source / p.embedder / p.modelId / p.dim
162
- */
170
+ // 消费一轮:有标记 ⇒ 跑坑源增量同步 ⇒ 成功才清标记(失败保留,下轮重试)
163
171
  export async function runPitfallHook({
164
- markerPath,
165
- store,
166
- source,
167
- embedder,
168
- modelId,
169
- dim,
170
- batchSize = 50,
171
- full = false,
172
- dryRun = false,
173
- log = () => {},
174
- now = () => Date.now(),
172
+ markerPath, store, source, embedder, modelId, dim,
173
+ batchSize = 50, full = false, dryRun = false,
174
+ log = () => {}, now = () => Date.now(),
175
175
  }) {
176
176
  const consumeStart = now();
177
- // 🔴 fail-closed 守卫(2026-10-10 实测踩到过):调用方若把**别的来源**当坑源传进来,
178
- // 本函数会一边按 pitfall 命名空间写向量、一边扫记忆行 ⇒ 静默把记忆行错标成坑向量。
179
- // ⇒ 来源名不对就**拒跑**(宁可少跑一轮,绝不污染命名空间)。
180
- const srcName = source && source.sourceName ? String(source.sourceName) : null;
181
- if (srcName !== 'pitfall') {
182
- throw new Error(
183
- `runPitfallHook 只接受坑源(sourceName='pitfall'),收到 ${srcName ?? '(未知来源/无 sourceName)'}`
184
- + ' ⇒ 拒跑(否则会把该来源的行错标成 pitfall 向量)',
185
- );
186
- }
187
- const r = readMarker(markerPath);
188
- if (r.error) {
189
- log(`[tick-hook] 标记文件读不动(本轮跳过、不清除):${r.error}`);
190
- return { ok: false, skipped: 'marker-unreadable', error: r.error, consumeStart };
177
+ assertPitfallSource(source);
178
+
179
+ const marker = readMarker(markerPath);
180
+ if (marker.error) {
181
+ log(`[tick-hook] 标记文件读不动(本轮跳过、不清除):${marker.error}`);
182
+ return { ok: false, skipped: 'marker-unreadable', error: marker.error, consumeStart };
191
183
  }
192
- if (!r.present) return { ok: true, skipped: 'no-marker', consumeStart };
184
+ if (!marker.present) return { ok: true, skipped: 'no-marker', consumeStart };
193
185
 
194
- log(`[tick-hook] 标记在位(updatedAt=${r.marker?.updatedAt ?? '?'},reason=${r.marker?.reason ?? '-'})⇒ 跑坑源增量同步`);
186
+ log(`[tick-hook] 标记在位(updatedAt=${marker.marker?.updatedAt ?? '?'},reason=${marker.marker?.reason ?? '-'})⇒ 跑坑源增量同步`);
195
187
  const stats = await runIncrementalSync({
196
- source,
197
- store,
198
- embedder,
199
- modelId,
200
- dim,
201
- sourceName: 'pitfall',
202
- batchSize,
203
- full,
204
- dryRun,
205
- log,
206
- now,
188
+ source, store, embedder, modelId, dim,
189
+ sourceName: 'pitfall', batchSize, full, dryRun, log, now,
207
190
  });
208
- let cleared = { cleared: false, reason: 'not-attempted' };
209
- if (dryRun) cleared = { cleared: false, reason: 'dry-run' };
210
- else if (stats.error) {
211
- log(`[tick-hook] 同步失败 ⇒ **保留标记**下轮重试:${stats.error}`);
212
- cleared = { cleared: false, reason: 'sync-failed-keep-marker' };
213
- } else {
214
- cleared = clearMarkerIfStale(markerPath, consumeStart);
215
- log(`[tick-hook] 清标记:${JSON.stringify(cleared)}`);
216
- }
217
- return { ok: !stats.error, consumeStart, marker: r.marker, stats, cleared };
191
+ const cleared = settleMarker({ markerPath, consumeStart, dryRun, stats, log });
192
+ return { ok: !stats.error, consumeStart, marker: marker.marker, stats, cleared };
218
193
  }