dsh-recall-plugin 1.5.0 → 1.6.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/lib/index.js CHANGED
@@ -12,10 +12,11 @@
12
12
  * 文件拆分见 lib/ 下各模块头注释;本文件只做接线,不承载业务逻辑。
13
13
  */
14
14
 
15
- import { createConfig } from './config.js'
15
+ import { createConfig, Config } from './config.js'
16
16
  import { createRuntime } from './store.js'
17
17
  import { createSnapshots } from './snapshots.js'
18
18
  import { createMaintenance } from './maintenance.js'
19
+ import { installSettingsSection } from '@deepseek-ai/dsh-settings'
19
20
 
20
21
  export const name = 'dsh-recall-plugin'
21
22
 
@@ -23,8 +24,13 @@ export const name = 'dsh-recall-plugin'
23
24
  // webServer(Client 半的 HTTP API 通道)。其余服务按需 ctx.get。
24
25
  export const inject = ['shell', 'sessions', 'webServer']
25
26
 
26
- // config 由 cordis.patch.yml 的 insert 行 config 键下发,用户在 profile 层
27
- // 按 id: recall 重述该行即可覆盖默认值(见 config.js 头注释)
27
+ // 入口配置 schema:cordis 加载器据此校验 insert 行 config 并填充默认值,
28
+ // 非法配置在插件加载时响亮失败(官方「插件配置」文档要求)。
29
+ export { Config }
30
+
31
+ // config 由 cordis.patch.yml 的 insert 行 config 键下发(schema 默认值兜底),
32
+ // 设置页「插件配置」卡片的用户覆盖经 settings namespace 热更新进 cfg
33
+ // (见下方 installSettingsSection 接线)
28
34
  export function apply(ctx, config) {
29
35
  const webServer = ctx.webServer
30
36
 
@@ -34,6 +40,27 @@ export function apply(ctx, config) {
34
40
  const maint = createMaintenance(ctx, rt, snaps, cfg)
35
41
  const state = rt.state
36
42
 
43
+ // ---- settings namespace「dsh-recall」:设置页「插件配置」分区正规接入 ----
44
+ // installSettingsSection(dsh-settings 官方辅助):settings 服务挂载后以
45
+ // 真 Config schema 注册 namespace、组合 base 取入口 config;服务卸载时
46
+ // 源回退入口 config。解析层 = schema 默认 → 组合 base → 用户文档(设置
47
+ // 卡片写入、dsh-settings 持久化),变更经 watch 热更新进运行中的 cfg
48
+ // (Object.assign 原地改,各域按调用时读取立即生效)。环境变量仍最高
49
+ // 优先(createConfig 内处理)。settings 服务未组装(非 web 部署)时
50
+ // 整段静默不运行,插件照常以入口配置工作。
51
+ let readSettings = () => config
52
+ function applyResolvedConfig(resolved) {
53
+ Object.assign(cfg, createConfig(resolved && typeof resolved === 'object' ? resolved : {}))
54
+ }
55
+ try {
56
+ installSettingsSection(ctx, 'dsh-recall', Config, config, {
57
+ setSource: (fn) => { readSettings = fn },
58
+ onChange: () => applyResolvedConfig(readSettings()),
59
+ })
60
+ } catch (error) {
61
+ rt.recordError('recall settings namespace skipped: ' + String(error))
62
+ }
63
+
37
64
  // 平台门控:win32 走 PowerShell 模板,linux/darwin 走 bash 模板
38
65
  // (ctx.shell 由 DSH 平台层单选挂载 pwsh/bash 执行器,见 dsh-shell README)。
39
66
  // 其余平台干净短路:init 返回 unsupported,Client 弹一次性提示;
@@ -51,6 +78,9 @@ export function apply(ctx, config) {
51
78
  // 也不是零成本(1 条 shell + 若干日志解压)。设置页打开、删除后刷新
52
79
  // 都会重拉,30s 缓存让二次打开即时;delete 与新快照落地时失效。
53
80
  let listCache = { at: 0, payload: null }
81
+ // 排除配置枚举缓存(30s):exclude-get 首次要遍历工作区、逐文件 shell 读,
82
+ // 设置页反复打开时不该每次重算;exclude-set 成功写入后立即失效。
83
+ let excludeCache = { at: 0, payload: null }
54
84
 
55
85
  // 会话标题缓存(apply 级跨请求共享):冷会话标题要 readSession 整日志
56
86
  // 解压 + 重放校验(大日志 10 秒级),绝不能挡列表首屏——list 只查
@@ -138,7 +168,9 @@ export function apply(ctx, config) {
138
168
  if (cwd) roots.add(cwd)
139
169
  }
140
170
  const byFile = new Map()
141
- for (const root of roots) {
171
+ // 并行解析全部 root:冷启动时每个 root 首次 resolveStore 可能触发 shell
172
+ // 建目录/写 root.txt,串行会随工作区数量线性变慢;Promise.all 让它们并发。
173
+ await Promise.all(Array.from(roots).map(async (root) => {
142
174
  try {
143
175
  const store = await rt.resolveStore(root)
144
176
  if (store && !byFile.has(store.excludeFile)) byFile.set(store.excludeFile, { store, roots: [] })
@@ -146,7 +178,7 @@ export function apply(ctx, config) {
146
178
  } catch (error) {
147
179
  /* 单个根解析失败只影响它自己,不拖垮整个列表 */
148
180
  }
149
- }
181
+ }))
150
182
  // 磁盘兜底:冷启动时会话注册表为空(惰性载入),但 home 容器目录可能
151
183
  // 早已存在(历史快照)。容器在 ⇒ 共享 exclude.txt 可编辑(哪怕从未
152
184
  // 写过、内容为空);容器不在 ⇒ 全新安装,让设置页显示引导文案。
@@ -278,6 +310,21 @@ export function apply(ctx, config) {
278
310
  return run
279
311
  }
280
312
 
313
+ // 通用并发限制器:冷会话标题/消息文本补齐会 readSession 整日志解压,
314
+ // 首次大量冷数据时全量 Promise.all 会同时压垮磁盘/CPU。限制同时最多
315
+ // CONCURRENCY 个任务,剩余排队执行;这是纯内存调度,不依赖额外依赖。
316
+ async function runLimited(tasks, concurrency) {
317
+ const limit = concurrency > 0 ? concurrency : 4
318
+ let index = 0
319
+ const workers = Array.from({ length: Math.min(limit, tasks.length) }, async () => {
320
+ while (index < tasks.length) {
321
+ const task = tasks[index++]
322
+ await task()
323
+ }
324
+ })
325
+ await Promise.all(workers)
326
+ }
327
+
281
328
  // 收集全量快照记录(内存 + 磁盘 dump 并集),供树形管理的按工作区/
282
329
  // 会话批量删除使用。返回 Map<消息ID, {id, root, sessionId, time}>;
283
330
  // 磁盘条目缺 root 时按 store 的 root.txt/内存映射补全(与 manage list
@@ -392,7 +439,9 @@ export function apply(ctx, config) {
392
439
  homeFallback: store ? !store.home : false
393
440
  }
394
441
  }
395
- return { ok: Boolean(root), root: root || null, notice }
442
+ // 顺带下发客户端行为开关(fillDraft 等):Client 无须为读配置单开请求,
443
+ // init 是每会话必经的预热通道
444
+ return { ok: Boolean(root), root: root || null, notice, config: { refillDraft: cfg.refillDraft } }
396
445
  },
397
446
 
398
447
  'snapshot-info': async (args) => {
@@ -443,14 +492,19 @@ export function apply(ctx, config) {
443
492
  // 设置页「撤回设置」标签的配置读取。不支持平台照常短路:Client
444
493
  // 显示不可用提示而不是空白表单,与 init 的 notice 语义对齐。
445
494
  if (!supported) return { ok: false, unsupported: true }
495
+ // 30s 结果缓存:首次进入要并行 resolveStore + 逐文件 shell 读,
496
+ // 二次打开/切标签不应重复付出这份代价;exclude-set 写入后失效。
497
+ if (excludeCache.payload && Date.now() - excludeCache.at < 30000) return excludeCache.payload
446
498
  const byFile = await listExcludeFiles()
447
- const files = []
448
- for (const [path, info] of byFile) {
499
+ // 并行读取各 exclude 文件内容:每个文件一条 shell,串行会放大延迟
500
+ const files = await Promise.all(Array.from(byFile.entries()).map(async ([path, info]) => {
449
501
  let content = ''
450
502
  try { content = await snaps.readExclude(info.store) } catch (error) { content = '' }
451
- files.push({ path, home: Boolean(info.store.home), roots: info.roots, content })
452
- }
453
- return { ok: true, files }
503
+ return { path, home: Boolean(info.store.home), roots: info.roots, content }
504
+ }))
505
+ const payload = { ok: true, files }
506
+ excludeCache = { at: Date.now(), payload }
507
+ return payload
454
508
  },
455
509
 
456
510
  'exclude-set': async (args) => {
@@ -463,6 +517,70 @@ export function apply(ctx, config) {
463
517
  const info = byFile.get(path)
464
518
  if (!info) return { ok: false, code: 'UNKNOWN_PATH', message: '未知的排除文件路径' }
465
519
  await snaps.writeExclude(info.store, content)
520
+ // 写入后立即失效:设置页保存后刷新必须看到最新内容
521
+ excludeCache.payload = null
522
+ return { ok: true }
523
+ },
524
+
525
+ // 设置页「插件配置」卡片读配置:resolved 全量值 + 用户已覆盖字段(来自
526
+ // settings.describe 的 user 层,字段在层里出现即用户覆盖)+ env 锁定
527
+ // 字段(环境变量优先级最高,设置改不动)+ 可写性(只读 provider 禁存)。
528
+ 'config-get': async () => {
529
+ const envLocks = {
530
+ gcSnaps: Boolean(process.env && process.env.DSH_RECALL_GC_SNAPS),
531
+ gcHours: Boolean(process.env && process.env.DSH_RECALL_GC_HOURS),
532
+ }
533
+ let overridden = {}
534
+ let writable = false
535
+ try {
536
+ const settings = ctx.get('settings')
537
+ if (settings && typeof settings.describe === 'function') {
538
+ const list = settings.describe()
539
+ const ours = (Array.isArray(list) ? list : []).find((d) => d && d.ns === 'dsh-recall')
540
+ if (ours && ours.user && typeof ours.user === 'object') overridden = ours.user
541
+ writable = settings.writable !== false
542
+ }
543
+ } catch (error) { /* describe 不可用按「无覆盖」处理 */ }
544
+ return {
545
+ ok: true,
546
+ values: {
547
+ gcSnaps: cfg.gcSnaps,
548
+ gcHours: cfg.gcHours,
549
+ maxFileBytes: cfg.maxFileBytes,
550
+ baseExcludes: cfg.baseExcludes.slice(),
551
+ refillDraft: cfg.refillDraft,
552
+ },
553
+ overridden,
554
+ envLocks,
555
+ writable,
556
+ }
557
+ },
558
+
559
+ // 设置页「插件配置」卡片存配置:白名单字段 + 类型清洗后经 settings.update
560
+ // 写进用户层(schema 校验失败会在持久化前 reject,错误信息回显卡片),
561
+ // watch 链路把新值热更新进 cfg,无需重启。
562
+ 'config-set': async (args) => {
563
+ const patch = args && args.patch && typeof args.patch === 'object' ? args.patch : {}
564
+ const clean = {}
565
+ if (patch.gcSnaps !== undefined) clean.gcSnaps = Number(patch.gcSnaps)
566
+ if (patch.gcHours !== undefined) clean.gcHours = Number(patch.gcHours)
567
+ if (patch.maxFileBytes !== undefined) clean.maxFileBytes = Number(patch.maxFileBytes)
568
+ if (patch.refillDraft !== undefined) clean.refillDraft = Boolean(patch.refillDraft)
569
+ if (patch.baseExcludes !== undefined) {
570
+ if (!Array.isArray(patch.baseExcludes)) return { ok: false, code: 'BAD_TYPE', message: 'baseExcludes 必须是字符串数组' }
571
+ clean.baseExcludes = patch.baseExcludes.filter((p) => typeof p === 'string' && p.trim())
572
+ }
573
+ if (!Object.keys(clean).length) return { ok: false, code: 'EMPTY_PATCH', message: '没有可写入的配置字段' }
574
+ let settings = null
575
+ try { settings = ctx.get('settings') } catch (error) { settings = null }
576
+ if (!settings || typeof settings.update !== 'function') {
577
+ return { ok: false, code: 'SETTINGS_UNAVAILABLE', message: '设置服务不可用:请在 profile 的 cordis.patch.yml 按 id: recall 覆盖配置' }
578
+ }
579
+ try {
580
+ await settings.update('dsh-recall', clean)
581
+ } catch (error) {
582
+ return { ok: false, code: 'SETTINGS_WRITE_FAILED', message: '配置写入失败:' + String(error && error.message ? error.message : error) }
583
+ }
466
584
  return { ok: true }
467
585
  },
468
586
 
@@ -542,10 +660,14 @@ export function apply(ctx, config) {
542
660
  // 重放校验,大日志 10 秒级——独立于列表让首屏即时。并发交给
543
661
  // sessionQuery 自带的 inspect 并发闸,这里全量并行发车。
544
662
  if (!supported) return { ok: false, unsupported: true }
545
- const ids = Array.isArray(args && args.sessionIds) ? args.sessionIds.map(String).slice(0, 100) : []
663
+ const ids = Array.from(new Set(
664
+ (Array.isArray(args && args.sessionIds) ? args.sessionIds.map(String) : []).filter(Boolean)
665
+ )).slice(0, 100)
546
666
  const out = {}
547
- await Promise.all(ids.map(async (sid) => {
548
- if (!sid || out[sid] !== undefined) return
667
+ // 并发限 4:冷标题 readSession 是重 IO,全量并发会把首次设置页
668
+ // 的 shell/日志解压都挤在一起;限制后列表本身不受影响,标题渐进补齐。
669
+ await runLimited(ids.map((sid) => async () => {
670
+ if (out[sid] !== undefined) return
549
671
  let title = liveTitleFast(sid)
550
672
  if (title === null) {
551
673
  const query = ctx.get('sessionQuery')
@@ -558,7 +680,7 @@ export function apply(ctx, config) {
558
680
  }
559
681
  sessionTitles.set(sid, title)
560
682
  out[sid] = title
561
- }))
683
+ }), 4)
562
684
  return { ok: true, titles: out }
563
685
  }
564
686
  if (op === 'messages') {
@@ -576,7 +698,7 @@ export function apply(ctx, config) {
576
698
  bySession.get(sid).push(mid)
577
699
  }
578
700
  const texts = {}
579
- await Promise.all(Array.from(bySession.entries()).map(async ([sid, mids]) => {
701
+ await runLimited(Array.from(bySession.entries()).map(([sid, mids]) => async () => {
580
702
  // 该会话所有消息都已缓存(含 null)时,不必 readSession 冷读
581
703
  const allCached = mids.every((mid) => messageTexts.has(String(sid) + '\u0000' + String(mid)))
582
704
  let log = null
@@ -604,16 +726,31 @@ export function apply(ctx, config) {
604
726
  messageTexts.set(key, text)
605
727
  texts[mid] = text
606
728
  }
607
- }))
729
+ }), 4)
608
730
  return { ok: true, messageTexts: texts }
609
731
  }
610
732
  if (op === 'usage') {
611
- const root = await rt.resolveRoot(sessionId)
612
- if (!root) return { ok: false, code: 'NO_ROOT', message: '无法解析当前工作区' }
613
- const store = state.stores.get(root)
614
- if (!store) return { ok: false, code: 'NO_STORE', message: '当前工作区尚未创建快照存储' }
615
- const out = await rt.runShell(rt.scripts.diskUsageScript(store.dir), { stdoutMaxBytes: 4096 })
616
- const bytes = parseInt(rt.scripts.stripBom(out).trim(), 10) || 0
733
+ let bytes = 0
734
+ if (sessionId) {
735
+ // 旧调用方(带会话上下文):单工作区占用
736
+ const root = await rt.resolveRoot(sessionId)
737
+ if (!root) return { ok: false, code: 'NO_ROOT', message: '无法解析当前工作区' }
738
+ const store = state.stores.get(root)
739
+ if (!store) return { ok: false, code: 'NO_STORE', message: '当前工作区尚未创建快照存储' }
740
+ const out = await rt.runShell(rt.scripts.diskUsageScript(store.dir), { stdoutMaxBytes: 4096 })
741
+ bytes = parseInt(rt.scripts.stripBom(out).trim(), 10) || 0
742
+ } else {
743
+ // 新调用方(settings.plugin.item 卡片无会话上下文):全部已知
744
+ // store 汇总。store 全集取内存缓存(启动预热填齐);单个失败
745
+ // 不影响汇总,best-effort。
746
+ for (const store of state.stores.values()) {
747
+ if (!store || !store.dir) continue
748
+ try {
749
+ const out = await rt.runShell(rt.scripts.diskUsageScript(store.dir), { stdoutMaxBytes: 4096 })
750
+ bytes += parseInt(rt.scripts.stripBom(out).trim(), 10) || 0
751
+ } catch (error) { /* 单 store 失败跳过 */ }
752
+ }
753
+ }
617
754
  return { ok: true, bytes }
618
755
  }
619
756
  if (op === 'delete') {
@@ -676,7 +813,11 @@ export function apply(ctx, config) {
676
813
  return { ok: true }
677
814
  }
678
815
  if (op === 'gc') {
679
- const done = await enqueue(() => maint.runGc(sessionId, true))
816
+ // 带会话上下文:只 gc 该会话的工作区;无上下文(设置卡片):
817
+ // 全部已知 store 逐个 gc。两者都排进串行队列,与快照互斥。
818
+ const done = sessionId
819
+ ? await enqueue(() => maint.runGc(sessionId, true))
820
+ : await enqueue(() => maint.runGcAll())
680
821
  return { ok: true, gc: Boolean(done) }
681
822
  }
682
823
  return { ok: false, code: 'UNKNOWN_OP', message: '未知的管理操作: ' + op }
@@ -730,16 +871,37 @@ export function apply(ctx, config) {
730
871
  // 启动预热:所有已存在工作区解析存储、重建索引与孤儿快照,
731
872
  // 并清理旧版项目内 blobs 目录(home 可用时)。
732
873
  // 不触发维护(gc/清理):开机预热应尽量轻,重活等第一条消息再按节流来。
733
- for (const session of ctx.sessions.list()) {
734
- const cwd = session && session.header && session.header.cwd
735
- if (!cwd) continue
736
- const sessionId = session.id
737
- Promise.resolve(rt.resolveStore(cwd))
738
- .then(() => rt.tryUpgradeToHome(cwd))
739
- .then((store) => rt.ensureGit(cwd, store))
740
- .then(() => snaps.loadIndex(cwd, sessionId))
741
- .then(() => snaps.rebuildOrphans(cwd, sessionId))
742
- .then(() => rt.cleanupLegacy(cwd))
743
- .catch(() => {})
744
- }
874
+ // 冷启动时 ctx.sessions.list() 常为空(惰性载入),必须叠加
875
+ // sessionQuery.listSessions() 冷元数据,否则设置页首次打开仍要现场建 store。
876
+ // apply 不是 async,这里用 IIFE 把冷元数据扫描包成异步任务。
877
+ ;(async () => {
878
+ const warmupRoots = new Map()
879
+ for (const session of ctx.sessions.list()) {
880
+ const cwd = session && session.header && session.header.cwd
881
+ if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, session.id)
882
+ }
883
+ const querySvc = ctx.get('sessionQuery')
884
+ if (querySvc && typeof querySvc.listSessions === 'function') {
885
+ try {
886
+ const records = await querySvc.listSessions()
887
+ for (const record of records || []) {
888
+ // listSessions 记录形如 {header, live, persisted},会话 id 在
889
+ // header.id——此前误用顶层 record.id(恒 undefined),预热重建
890
+ // 的孤儿快照 sessionId 记为空,树形管理里会落进「已删除会话」。
891
+ const id = record && record.header && record.header.id ? record.header.id : null
892
+ const cwd = record && record.header && record.header.cwd
893
+ if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, id)
894
+ }
895
+ } catch (error) { /* 冷元数据不可用则退回 live 注册表 */ }
896
+ }
897
+ for (const [cwd, sessionId] of warmupRoots) {
898
+ Promise.resolve(rt.resolveStore(cwd))
899
+ .then(() => rt.tryUpgradeToHome(cwd))
900
+ .then((store) => rt.ensureGit(cwd, store))
901
+ .then(() => snaps.loadIndex(cwd, sessionId))
902
+ .then(() => snaps.rebuildOrphans(cwd, sessionId))
903
+ .then(() => rt.cleanupLegacy(cwd))
904
+ .catch(() => {})
905
+ }
906
+ })()
745
907
  }
@@ -11,13 +11,12 @@
11
11
  * 接线),因此 gc/清理与快照天然互斥,不存在 git 锁竞态。
12
12
  */
13
13
 
14
- // gc 节流阈值来自 config 域(用户可在 profile 的 cordis.patch.yml 覆盖,
15
- // 环境变量 DSH_RECALL_GC_SNAPS/GC_HOURS 仍最高优先,见 config.js):
14
+ // gc 节流阈值来自 config 域(设置页「插件配置」卡片可实时改写 cfg,
15
+ // 因此这里按调用时取值而不是工厂创建时快照;环境变量
16
+ // DSH_RECALL_GC_SNAPS/GC_HOURS 仍最高优先,见 config.js):
16
17
  // 每 gcSnaps 条快照或距上次 gc gcHours 小时,先到先触发。默认「50 条或
17
18
  // 24 小时」——重活(gc)一天至多一次的量级,轻会话用户也不会等太久。
18
19
  export function createMaintenance(ctx, rt, snaps, config) {
19
- const GC_SNAPS = config.gcSnaps
20
- const GC_MS = config.gcHours * 3600000
21
20
  const sessions = ctx.sessions
22
21
  const state = rt.state
23
22
  // 平台选择的脚本模板(gc/purge 两套模板同名导出)
@@ -97,7 +96,7 @@ export function createMaintenance(ctx, rt, snaps, config) {
97
96
  const last = state.gcLastAt.get(store.git) || 0
98
97
  const count = (state.gcCount.get(store.git) || 0) + 1
99
98
  state.gcCount.set(store.git, count)
100
- if (!force && count < GC_SNAPS && now - last < GC_MS) return false
99
+ if (!force && count < config.gcSnaps && now - last < config.gcHours * 3600000) return false
101
100
  state.gcCount.set(store.git, 0)
102
101
  try {
103
102
  await sweepDeletedSessions()
@@ -109,12 +108,38 @@ export function createMaintenance(ctx, rt, snaps, config) {
109
108
  return true
110
109
  }
111
110
 
111
+ // 全局 gc(设置卡片没有会话上下文):清理扫描一次 + 逐 store gc。
112
+ // store 全集取内存缓存(启动预热与历次操作会填齐已知工作区);逐个
113
+ // best-effort,单个失败记错误继续。调用方(manage 端点)把它排进同一条
114
+ // 串行队列,与快照天然互斥,无 git 锁竞态。
115
+ async function runGcAll() {
116
+ const stores = Array.from(new Set(Array.from(state.stores.values()).filter(Boolean)))
117
+ if (!stores.length || !state.gitExe) return false
118
+ try {
119
+ await sweepDeletedSessions()
120
+ } catch (error) {
121
+ rt.recordError('recall sweep failed: ' + String(error))
122
+ }
123
+ let done = 0
124
+ for (const store of stores) {
125
+ try {
126
+ await rt.runShell(S.gcScript(store, state.gitExe), { timeoutMs: 600000, stdoutMaxBytes: 4096 })
127
+ done++
128
+ } catch (error) {
129
+ rt.recordError('recall gc failed for ' + (store && store.git) + ': ' + String(error))
130
+ }
131
+ state.gcLastAt.set(store.git, Date.now())
132
+ state.gcCount.set(store.git, 0)
133
+ }
134
+ return true
135
+ }
136
+
112
137
  // 每条消息快照后串行调用(见 index.js 事件接线)
113
138
  async function maybeMaintain(sessionId) {
114
139
  await runGc(sessionId, false)
115
140
  }
116
141
 
117
- // 模块收敛:只导出唯一被装配层消费的入口;sweepDeletedSessions /
118
- // purgeSession 是 runGc 的内部步骤,不对外暴露面
119
- return { maybeMaintain, runGc }
142
+ // 模块收敛:只导出被装配层消费的入口;sweepDeletedSessions /
143
+ // purgeSession 是 gc 的内部步骤,不对外暴露面
144
+ return { maybeMaintain, runGc, runGcAll }
120
145
  }