@kanadego/dsh-heartbeat 1.7.3 → 1.8.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/dist/index.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import {
2
2
  adviseRefillWander,
3
3
  adviseWander,
4
- appendAuditLine,
5
- applyOpsToDoc,
6
- atomicWriteJsonSync,
4
+ appendEntry,
5
+ buildWeeklyPrompt,
7
6
  completeWander,
8
7
  createPathGuard,
9
8
  deepMerge,
@@ -11,19 +10,21 @@ import {
11
10
  ensureRegistered,
12
11
  installBundledPreset,
13
12
  ledgerFilePath,
13
+ listWeeklyReports,
14
14
  loadPolicy,
15
- loadProfile,
16
- loadProfileSchema,
15
+ markDone,
17
16
  pendingOlderThan,
18
- persistWithJournal,
19
- profileFilePath,
20
- pruneAuditFile,
21
- runDeterministicAging,
17
+ readLedger,
18
+ readWeeklyReport,
19
+ renderTemplateReport,
20
+ saveWeeklyReport,
22
21
  scanPending,
23
22
  sendNewMessageHint,
23
+ sendWeeklyReadyHint,
24
24
  updateUserPolicy,
25
- userPresetRoot
26
- } from "./chunk-ON4MSU6E.js";
25
+ userPresetRoot,
26
+ weeklyDue
27
+ } from "./chunk-SQJLKP7L.js";
27
28
  import {
28
29
  getRuntime,
29
30
  setRuntime
@@ -39,14 +40,35 @@ import {
39
40
  restoreSeed,
40
41
  seedsFilePath,
41
42
  surfaceSeed
42
- } from "./chunk-SVP2NDRF.js";
43
+ } from "./chunk-MXJPX3FM.js";
43
44
  import {
44
45
  dedupeItems,
45
46
  inboxClear,
46
47
  inboxCount,
47
48
  inboxDrain,
48
49
  inboxFilePath
49
- } from "./chunk-4UE74TUB.js";
50
+ } from "./chunk-63FUJLIF.js";
51
+ import {
52
+ snapshotIfDue
53
+ } from "./chunk-RB3NLLGF.js";
54
+ import {
55
+ appendAuditLine,
56
+ applyOpsToDoc,
57
+ atomicWriteJsonSync,
58
+ journalFilePath,
59
+ loadProfile,
60
+ loadProfileSchema,
61
+ persistWithJournal,
62
+ profileFilePath,
63
+ pruneAuditFile,
64
+ readAuditLines,
65
+ runDeterministicAging
66
+ } from "./chunk-VIZNIQLK.js";
67
+ import {
68
+ addBinding,
69
+ loadBindings,
70
+ removeBinding
71
+ } from "./chunk-J6ZTRFFW.js";
50
72
  import {
51
73
  decryptFile,
52
74
  encryptFile,
@@ -57,12 +79,7 @@ import {
57
79
  saveEncryptedText,
58
80
  saveJson,
59
81
  writeText
60
- } from "./chunk-LLD7LUNN.js";
61
- import {
62
- addBinding,
63
- loadBindings,
64
- removeBinding
65
- } from "./chunk-J6ZTRFFW.js";
82
+ } from "./chunk-IFTFDHZX.js";
66
83
 
67
84
  // node_modules/@deepseek-ai/cosmokit/lib/index.js
68
85
  function isNullable(value) {
@@ -975,7 +992,7 @@ function saveUiConfig(guard, settingsDir, patch) {
975
992
 
976
993
  // src/core/orchestrator.ts
977
994
  import { randomUUID as randomUUID2 } from "crypto";
978
- import fs9 from "fs";
995
+ import fs10 from "fs";
979
996
  import path9 from "path";
980
997
 
981
998
  // src/env/envpulse.ts
@@ -1179,10 +1196,10 @@ function collectScreen(guard, paths, now = Date.now()) {
1179
1196
  } catch {
1180
1197
  }
1181
1198
  }
1182
- const { shot_path: _drop, ...clean } = meta;
1199
+ const { shot_path: _drop, ...clean2 } = meta;
1183
1200
  void _drop;
1184
1201
  encJson = path4.join(paths.tmpDir, `screen.enc.${now}.json`);
1185
- fs4.writeFileSync(encJson, JSON.stringify(clean, null, 1), "utf8");
1202
+ fs4.writeFileSync(encJson, JSON.stringify(clean2, null, 1), "utf8");
1186
1203
  encryptFile(guard, encJson, screenJsonPath(paths));
1187
1204
  return {
1188
1205
  ok: true,
@@ -1733,7 +1750,7 @@ async function runConsolidation(guard, paths, policy, llm, now = Date.now()) {
1733
1750
  const report = applyOpsToDoc(guard, paths.dataDir, doc, profileOpsCast, schema, policy, now);
1734
1751
  const aged = runDeterministicAging(doc, policy, now);
1735
1752
  persistWithJournal(guard, paths.dataDir, doc, { runId, applied: report.applied, rejected: report.rejected });
1736
- const { addSeed: addSeed2, seedsFilePath: seedsFilePath2 } = await import("./pool-YAIBE2L5.js");
1753
+ const { addSeed: addSeed2, seedsFilePath: seedsFilePath2 } = await import("./pool-JQC5JWIZ.js");
1737
1754
  let chatSeedsAdded = 0;
1738
1755
  for (const cs of chatSeeds) {
1739
1756
  try {
@@ -1863,6 +1880,125 @@ function recordPresence(paths, env, now = Date.now()) {
1863
1880
  if (state.days.length > 30) state.days.shift();
1864
1881
  saveRhythm(paths, state);
1865
1882
  }
1883
+ function summarizeRhythm(paths) {
1884
+ const state = loadRhythm(paths);
1885
+ const score = [];
1886
+ for (const wd of Object.keys(state.histogram)) {
1887
+ for (const h of Object.keys(state.histogram[wd])) {
1888
+ const cell = state.histogram[wd][h];
1889
+ score.push({ key: `${h}\u65F6(\u5468${"\u65E5\u4E00\u4E8C\u4E09\u56DB\u4E94\u516D"[Number(wd)]})`, active: cell.active });
1890
+ }
1891
+ }
1892
+ score.sort((a, b) => b.active - a.active);
1893
+ return {
1894
+ daysSampled: state.days.length,
1895
+ peakHours: score.filter((s) => s.active > 0).slice(0, 6).map((s) => s.key)
1896
+ };
1897
+ }
1898
+
1899
+ // src/weekly/collect.ts
1900
+ import fs9 from "fs";
1901
+ var DAY_MS2 = 864e5;
1902
+ var WINDOW_DAYS = 7;
1903
+ function inWindow(ts, start, end) {
1904
+ const t = Date.parse(ts);
1905
+ return Number.isFinite(t) && t >= start && t <= end;
1906
+ }
1907
+ function trim(text, max) {
1908
+ const t = text.replace(/\s+/g, " ").trim();
1909
+ return t.length > max ? t.slice(0, max - 1) + "\u2026" : t;
1910
+ }
1911
+ function collectWeeklyFacts(guard, paths, policy, now = Date.now()) {
1912
+ const start = now - WINDOW_DAYS * DAY_MS2;
1913
+ const end = now;
1914
+ let spoken = 0;
1915
+ let silent = 0;
1916
+ let observedItems = 0;
1917
+ const reasons = /* @__PURE__ */ new Map();
1918
+ try {
1919
+ for (const e of readAuditLines(paths.logsDir + "/heartbeat.jsonl")) {
1920
+ const ts = e.ts ?? "";
1921
+ if (!inWindow(ts, start, end)) continue;
1922
+ const ev = e.event;
1923
+ if (ev === "spoke") spoken += 1;
1924
+ else if (ev === "silent") {
1925
+ silent += 1;
1926
+ const reason = String(e.reason ?? "unknown").slice(0, 60);
1927
+ reasons.set(reason, (reasons.get(reason) ?? 0) + 1);
1928
+ } else if (ev === "observed") {
1929
+ observedItems += Number(e.added ?? 0);
1930
+ }
1931
+ }
1932
+ } catch {
1933
+ }
1934
+ const profileAdds = [];
1935
+ try {
1936
+ const raw = fs9.readFileSync(journalFilePath(paths.dataDir), "utf8");
1937
+ for (const line of raw.split("\n")) {
1938
+ if (!line.trim()) continue;
1939
+ try {
1940
+ const rec = JSON.parse(line);
1941
+ if (!inWindow(rec.ts ?? "", start, end)) continue;
1942
+ for (const op of rec.applied ?? []) {
1943
+ if (op.op !== "ADD" || !op.content) continue;
1944
+ profileAdds.push({
1945
+ partition: String(op.partition ?? "?"),
1946
+ topic: String(op.subTopic ? `${op.topic}/${op.subTopic}` : op.topic ?? "?"),
1947
+ content: trim(op.content, 60)
1948
+ });
1949
+ }
1950
+ } catch {
1951
+ }
1952
+ }
1953
+ } catch {
1954
+ }
1955
+ const ledger = { added: [], done: [], stale: [] };
1956
+ try {
1957
+ const { entries } = readLedger(guard, ledgerFilePath(paths.dataDir));
1958
+ for (const e of entries) {
1959
+ const t = Date.parse(`${e.date}T${e.time}:00`);
1960
+ if (Number.isFinite(t) && t >= start && t <= end) {
1961
+ (e.status === "done" ? ledger.done : ledger.added).push(trim(e.text, 40));
1962
+ }
1963
+ }
1964
+ const staleCutoffDays = 12;
1965
+ for (const e of pendingOlderThan(guard, ledgerFilePath(paths.dataDir), staleCutoffDays, now)) {
1966
+ const days = Math.max(1, Math.round((now - Date.parse(`${e.date}T${e.time}:00`)) / DAY_MS2));
1967
+ ledger.stale.push({ text: trim(e.text, 40), days });
1968
+ }
1969
+ } catch {
1970
+ }
1971
+ const seeds = { added: [], consumed: [], waiting: [], poolActive: 0 };
1972
+ try {
1973
+ const db = loadPool(guard, seedsFilePath(paths.dataDir));
1974
+ seeds.poolActive = activeSeeds(db).length;
1975
+ for (const s of db.seeds) {
1976
+ if (inWindow(s.bornAt, start, end)) seeds.added.push(trim(s.text, 40));
1977
+ if (s.status === "archived" && s.retireReason === "consumed" && s.retiredAt && inWindow(s.retiredAt, start, end)) {
1978
+ seeds.consumed.push(trim(s.text, 40));
1979
+ }
1980
+ }
1981
+ seeds.waiting = activeSeeds(db).filter((s) => s.used === 0 && s.category === "topic").sort((a, b) => a.bornAt < b.bornAt ? 1 : -1).slice(0, 5).map((s) => trim(s.text, 40));
1982
+ } catch {
1983
+ }
1984
+ let peakHours = [];
1985
+ try {
1986
+ peakHours = summarizeRhythm(paths).peakHours;
1987
+ } catch {
1988
+ }
1989
+ return {
1990
+ windowStart: new Date(start).toISOString(),
1991
+ windowEnd: new Date(end).toISOString(),
1992
+ spoken,
1993
+ silent,
1994
+ silentTopReasons: [...reasons.entries()].map(([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count).slice(0, 3),
1995
+ observedItems,
1996
+ profileAdds,
1997
+ ledger,
1998
+ seeds,
1999
+ peakHours
2000
+ };
2001
+ }
1866
2002
 
1867
2003
  // src/core/orchestrator.ts
1868
2004
  var reschedule = null;
@@ -1889,11 +2025,24 @@ function sessionEventCount(session) {
1889
2025
  if (typeof session.seq === "number") return session.seq;
1890
2026
  return sessionEvents(session).length;
1891
2027
  }
2028
+ var HOME_ROTATE_EVENT_COUNT = 3e3;
2029
+ function isContextOverflowError(msg) {
2030
+ return /CONTEXT_WINDOW_EXCEEDED|context overflow/i.test(msg);
2031
+ }
2032
+ function shouldRotateHome(input) {
2033
+ return input.eventCount >= (input.threshold ?? HOME_ROTATE_EVENT_COUNT);
2034
+ }
1892
2035
  var IDLE_WAIT_TIMEOUT_MS = 24e4;
1893
2036
  var EXPRESSION_IDLE_WAIT_MS = 6e5;
1894
2037
  var agentPromise = null;
1895
2038
  var beating = false;
1896
2039
  var beatCancel;
2040
+ var homeRotatePending = null;
2041
+ function markHomeRotateIfOverflow(error) {
2042
+ if (homeRotatePending === null && isContextOverflowError(String(error))) {
2043
+ homeRotatePending = `context overflow: ${String(error).slice(0, 120)}`;
2044
+ }
2045
+ }
1897
2046
  var lastBeat = null;
1898
2047
  function getLastBeat() {
1899
2048
  return lastBeat;
@@ -1938,49 +2087,132 @@ function defaultAgentOptions(ctx) {
1938
2087
  }
1939
2088
  return void 0;
1940
2089
  }
2090
+ var ENGINE_ROOM_EXTRA_TOOLS = ["compress", "decompress", "acp_status"];
2091
+ function buildEngineRoomAllowList(globalNames, configExtras = []) {
2092
+ const global = new Set(globalNames);
2093
+ const allow = ["web_search"];
2094
+ for (const name2 of [...ENGINE_ROOM_EXTRA_TOOLS, ...configExtras.map((t) => t.trim()).filter(Boolean)]) {
2095
+ if (global.has(name2) && !allow.includes(name2)) allow.push(name2);
2096
+ }
2097
+ return allow;
2098
+ }
2099
+ function makeHomeSetup(deps) {
2100
+ const { ctx, paths } = deps;
2101
+ return async (agentCtx) => {
2102
+ const notes = [];
2103
+ const presetId = deps.agentPreset ?? "heartbeat";
2104
+ try {
2105
+ const presets = agentCtx.get("agentPresets");
2106
+ if (typeof presets?.mount !== "function") {
2107
+ notes.push("preset=no-api");
2108
+ } else {
2109
+ const preset = await presets.mount(agentCtx, presetId);
2110
+ const joined = preset?.id;
2111
+ notes.push(`preset=mounted(${joined ?? presetId})`);
2112
+ }
2113
+ } catch (e) {
2114
+ notes.push(`preset=threw(${String(e).slice(0, 200)})`);
2115
+ }
2116
+ const tools = agentCtx.get("tools");
2117
+ if (typeof tools?.restrict !== "function") {
2118
+ notes.push("restrict=no-api");
2119
+ } else {
2120
+ let globalNames = [];
2121
+ try {
2122
+ globalNames = (tools.schemas?.() ?? []).map((s) => String(s?.name ?? ""));
2123
+ } catch {
2124
+ }
2125
+ const allow = buildEngineRoomAllowList(globalNames, deps.extraTools ?? []);
2126
+ try {
2127
+ tools.restrict({ allow });
2128
+ notes.push(`restrict=ok allow=${allow.join("|")}`);
2129
+ } catch (e) {
2130
+ notes.push(`restrict=threw(${String(e).slice(0, 200)})`);
2131
+ }
2132
+ try {
2133
+ const visible = globalNames.sort();
2134
+ notes.push(`visibleGlobal=${visible.length > 0 ? visible.join(",") : "(empty)"}`);
2135
+ } catch (e) {
2136
+ notes.push(`visibleGlobal=threw(${String(e).slice(0, 60)})`);
2137
+ }
2138
+ }
2139
+ ctx.logger.info("heartbeat: tool policy %s", notes.join(" "));
2140
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "tool_policy", policy: notes.join(" ") });
2141
+ };
2142
+ }
2143
+ async function rotateHome(deps, reason) {
2144
+ const { ctx, guard, paths } = deps;
2145
+ const old = readBeatState(guard, paths).sessionId;
2146
+ const freshId = `session-${randomUUID2()}`;
2147
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
2148
+ event: "home_rotate_start",
2149
+ from: old,
2150
+ to: freshId,
2151
+ reason
2152
+ });
2153
+ const handle = await withTimeout(
2154
+ Promise.resolve(ctx.agents.create({
2155
+ sessionId: freshId,
2156
+ meta: { cwd: paths.dataDir },
2157
+ ...defaultAgentOptions(ctx) ? { agentOptions: defaultAgentOptions(ctx) } : {},
2158
+ setup: makeHomeSetup(deps)
2159
+ })),
2160
+ 3e4,
2161
+ "home rotate create timeout (30s)"
2162
+ );
2163
+ const agent = unwrapHomeHandle(handle);
2164
+ const realId = agent.session?.id ?? freshId;
2165
+ writeBeatState(guard, paths, { sessionId: realId });
2166
+ agentPromise = null;
2167
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
2168
+ event: "home_rotate_done",
2169
+ from: old,
2170
+ to: realId,
2171
+ reason
2172
+ });
2173
+ ctx.logger.info("heartbeat: home rotated %s -> %s (%s)", old ?? "(none)", realId, reason);
2174
+ if (shouldArchiveRotatedHome({ old, realId, enabled: deps.policy.heartbeat.archiveRotatedHome !== false })) {
2175
+ archiveSessionBestEffort(deps, old, reason);
2176
+ }
2177
+ }
2178
+ function shouldArchiveRotatedHome(input) {
2179
+ return Boolean(input.old) && input.old !== input.realId && input.enabled;
2180
+ }
2181
+ function archiveSessionBestEffort(deps, sessionId, why) {
2182
+ const audit = (entry) => {
2183
+ try {
2184
+ appendAuditLine(deps.paths.logsDir + "/heartbeat.jsonl", entry);
2185
+ } catch {
2186
+ }
2187
+ };
2188
+ try {
2189
+ const injectable = deps.ctx;
2190
+ injectable.inject(["workspaceRegistry"], (scoped) => {
2191
+ const registry = scoped.workspaceRegistry;
2192
+ if (!registry || typeof registry.archiveSession !== "function") {
2193
+ audit({ event: "home_rotate_archive", ok: false, sessionId, why, error: "workspaceRegistry unavailable" });
2194
+ return;
2195
+ }
2196
+ void Promise.resolve(registry.archiveSession(sessionId, { stopActivity: true })).then(
2197
+ () => audit({ event: "home_rotate_archive", ok: true, sessionId, why }),
2198
+ (e) => audit({ event: "home_rotate_archive", ok: false, sessionId, why, error: String(e).slice(0, 160) })
2199
+ );
2200
+ });
2201
+ } catch (e) {
2202
+ audit({ event: "home_rotate_archive", ok: false, sessionId, why, error: String(e).slice(0, 160) });
2203
+ }
2204
+ }
2205
+ function unwrapHomeHandle(handle) {
2206
+ return handle.agent ?? handle;
2207
+ }
1941
2208
  async function ensureAgent(deps) {
1942
2209
  if (agentPromise) return agentPromise;
1943
2210
  const { ctx, paths, guard } = deps;
1944
2211
  const agentOptions = defaultAgentOptions(ctx);
2212
+ const setup = makeHomeSetup(deps);
1945
2213
  agentPromise = (async () => {
1946
2214
  const saved = readBeatState(guard, paths);
1947
2215
  const savedId = saved.sessionId;
1948
- const setup = async (agentCtx) => {
1949
- const notes = [];
1950
- const presetId = deps.agentPreset ?? "heartbeat";
1951
- try {
1952
- const presets = agentCtx.get("agentPresets");
1953
- if (typeof presets?.mount !== "function") {
1954
- notes.push("preset=no-api");
1955
- } else {
1956
- const preset = await presets.mount(agentCtx, presetId);
1957
- const joined = preset?.id;
1958
- notes.push(`preset=mounted(${joined ?? presetId})`);
1959
- }
1960
- } catch (e) {
1961
- notes.push(`preset=threw(${String(e).slice(0, 200)})`);
1962
- }
1963
- const tools = agentCtx.get("tools");
1964
- if (typeof tools?.restrict !== "function") {
1965
- notes.push("restrict=no-api");
1966
- } else {
1967
- const allow = ["web_search"];
1968
- try {
1969
- tools.restrict({ allow });
1970
- notes.push(`restrict=ok allow=${allow.join("|")}`);
1971
- } catch (e) {
1972
- notes.push(`restrict=threw(${String(e).slice(0, 200)})`);
1973
- }
1974
- try {
1975
- const visible = (tools.schemas?.() ?? []).map((s) => String(s?.name ?? "?")).sort();
1976
- notes.push(`visibleGlobal=${visible.length > 0 ? visible.join(",") : "(empty)"}`);
1977
- } catch (e) {
1978
- notes.push(`visibleGlobal=threw(${String(e).slice(0, 60)})`);
1979
- }
1980
- }
1981
- ctx.logger.info("heartbeat: tool policy %s", notes.join(" "));
1982
- appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "tool_policy", policy: notes.join(" ") });
1983
- };
1984
2216
  const unwrap = (handle) => handle.agent ?? handle;
1985
2217
  try {
1986
2218
  let agent;
@@ -2112,6 +2344,7 @@ async function agentTurn(deps, agent, prompt, label, idleWaitMs = IDLE_WAIT_TIME
2112
2344
  ...turnError ? { turnError } : {},
2113
2345
  window: shapes.slice(0, 12)
2114
2346
  });
2347
+ if (turnError) markHomeRotateIfOverflow(turnError);
2115
2348
  return "";
2116
2349
  }
2117
2350
  async function withTimeout(p, ms, label) {
@@ -2143,6 +2376,7 @@ async function maintenancePhase(bc) {
2143
2376
  gcPool(guard, seedsFilePath(paths.dataDir), policy, now);
2144
2377
  pruneAuditFile(paths.logsDir + "/envpulse.jsonl", policy.retention.envPulseHours * 36e5, now);
2145
2378
  pruneAuditFile(paths.logsDir + "/heartbeat.jsonl", policy.retention.decisionLogDays * 864e5, now);
2379
+ snapshotIfDue(guard, paths.dataDir, paths.logsDir + "/heartbeat.jsonl", "maintenance-threshold", now);
2146
2380
  const cons = shouldConsolidate(guard, paths, policy, now);
2147
2381
  if (cons.due) {
2148
2382
  const llm = async (prompt) => {
@@ -2174,14 +2408,14 @@ async function observeBoundSessions(bc) {
2174
2408
  const { deps, now } = bc;
2175
2409
  const { guard, paths } = deps;
2176
2410
  const { loadBindings: loadBindings2, observeTargets } = await import("./bindings-XPPSKILN.js");
2177
- const { inboxAppend, inboxFilePath: inboxFilePath2 } = await import("./inbox-MMLHISQV.js");
2411
+ const { inboxAppend, inboxFilePath: inboxFilePath2 } = await import("./inbox-57NHQHY3.js");
2178
2412
  const data = loadBindings2(guard, paths.settingsDir);
2179
2413
  const targets = observeTargets(data);
2180
2414
  if (targets.length === 0) return;
2181
2415
  const cursorFile = path9.join(paths.dataDir, "cursors.json");
2182
2416
  let cursors = {};
2183
2417
  try {
2184
- cursors = JSON.parse(fs9.readFileSync(cursorFile, "utf8"));
2418
+ cursors = JSON.parse(fs10.readFileSync(cursorFile, "utf8"));
2185
2419
  } catch {
2186
2420
  }
2187
2421
  const inboxFile = inboxFilePath2(paths.dataDir);
@@ -2193,7 +2427,8 @@ async function observeBoundSessions(bc) {
2193
2427
  const cursor = cursors[b.sessionId] ?? 0;
2194
2428
  let last = cursor;
2195
2429
  let added = 0;
2196
- for (let i = cursor; i < events.length && added < 10; i++) {
2430
+ const { maxChars, perBeat } = deps.policy.observe;
2431
+ for (let i = cursor; i < events.length && added < perBeat; i++) {
2197
2432
  const e = events[i];
2198
2433
  if (e.type !== "user/message") continue;
2199
2434
  const d = e.data;
@@ -2204,7 +2439,7 @@ async function observeBoundSessions(bc) {
2204
2439
  kind: "chat",
2205
2440
  at: new Date(now).toISOString(),
2206
2441
  ref: `cursors.json#${b.sessionId}:${i}`,
2207
- note: text.split(/[。!?\n]/)[0].slice(0, 80)
2442
+ note: text.split(/[。!?\n]/)[0].slice(0, maxChars)
2208
2443
  });
2209
2444
  added += 1;
2210
2445
  last = i + 1;
@@ -2219,6 +2454,50 @@ async function observeBoundSessions(bc) {
2219
2454
  }
2220
2455
  }
2221
2456
  }
2457
+ async function weeklyPhase(bc) {
2458
+ const { deps, agent, now } = bc;
2459
+ const { guard, paths, policy } = deps;
2460
+ if (policy.weekly.enabled === false) return;
2461
+ if (!weeklyDue(guard, paths.dataDir, now)) return;
2462
+ const audit = (entry) => {
2463
+ try {
2464
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", entry);
2465
+ } catch {
2466
+ }
2467
+ };
2468
+ if (!agent) {
2469
+ audit({ event: "weekly_skipped", reason: "agentless beat" });
2470
+ return;
2471
+ }
2472
+ try {
2473
+ const facts = collectWeeklyFacts(guard, paths, policy, now);
2474
+ let text = "";
2475
+ let source = "template";
2476
+ try {
2477
+ text = (await agentTurn(deps, agent, buildWeeklyPrompt(facts), "weekly", IDLE_WAIT_TIMEOUT_MS)).trim();
2478
+ if (text) source = "llm";
2479
+ } catch (e) {
2480
+ audit({ event: "weekly_llm_failed", error: String(e).slice(0, 160) });
2481
+ }
2482
+ if (!text) text = renderTemplateReport(facts);
2483
+ saveWeeklyReport(guard, paths.dataDir, {
2484
+ start: facts.windowStart,
2485
+ end: facts.windowEnd,
2486
+ generatedAt: new Date(now).toISOString(),
2487
+ source,
2488
+ text
2489
+ });
2490
+ audit({ event: "weekly_generated", source, chars: text.length });
2491
+ if (!inQuietHours(policy, now)) {
2492
+ try {
2493
+ sendWeeklyReadyHint(paths);
2494
+ } catch {
2495
+ }
2496
+ }
2497
+ } catch (e) {
2498
+ audit({ event: "weekly_failed", error: String(e).slice(0, 160) });
2499
+ }
2500
+ }
2222
2501
  function ctx_getAgent(deps, sessionId) {
2223
2502
  try {
2224
2503
  const agent = deps.ctx.agents.get(sessionId);
@@ -2555,6 +2834,20 @@ async function beat(deps) {
2555
2834
  }
2556
2835
  };
2557
2836
  }
2837
+ if (agent && homeRotatePending === null && shouldRotateHome({ eventCount: sessionEventCount(agent.session) })) {
2838
+ homeRotatePending = `size threshold (${sessionEventCount(agent.session)} events \u2265 ${HOME_ROTATE_EVENT_COUNT})`;
2839
+ }
2840
+ if (agent && homeRotatePending !== null) {
2841
+ try {
2842
+ await rotateHome(deps, homeRotatePending);
2843
+ homeRotatePending = null;
2844
+ } catch (e) {
2845
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "home_rotate_failed", error: String(e).slice(0, 160) });
2846
+ }
2847
+ writeBeatStatus(deps, { beatStart, wandered: false });
2848
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "silent", reason: "home rotated, next beat starts fresh" });
2849
+ return;
2850
+ }
2558
2851
  let wandered = false;
2559
2852
  if (agent) {
2560
2853
  deferredRetries = 0;
@@ -2565,6 +2858,7 @@ async function beat(deps) {
2565
2858
  } else {
2566
2859
  await maintenancePhase(bc);
2567
2860
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "phase_done", phase: "maintenance" });
2861
+ await weeklyPhase(bc);
2568
2862
  await collectPhase(bc);
2569
2863
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "phase_done", phase: "collect" });
2570
2864
  wandered = await wanderPhase(bc);
@@ -2586,6 +2880,7 @@ async function beat(deps) {
2586
2880
  writeBeatStatus(deps, { beatStart, wandered });
2587
2881
  } catch (e) {
2588
2882
  deps.ctx.logger.error("heartbeat: beat failed: %s", String(e).slice(0, 200));
2883
+ markHomeRotateIfOverflow(e);
2589
2884
  try {
2590
2885
  appendAuditLine(deps.paths.logsDir + "/heartbeat.jsonl", { event: "beat_error", error: String(e).slice(0, 200) });
2591
2886
  noteBeat("error", { reason: String(e).slice(0, 120) });
@@ -2643,12 +2938,12 @@ function startOrchestrator(deps) {
2643
2938
 
2644
2939
  // src/rpc.ts
2645
2940
  import { spawn as spawn2 } from "child_process";
2646
- import fs12 from "fs";
2941
+ import fs13 from "fs";
2647
2942
  import os from "os";
2648
2943
  import path12 from "path";
2649
2944
 
2650
2945
  // src/browse/interests-edit.ts
2651
- import fs10 from "fs";
2946
+ import fs11 from "fs";
2652
2947
  import path10 from "path";
2653
2948
  var MAX_INTERESTS = 32;
2654
2949
  var MAX_INTEREST_LEN = 60;
@@ -2662,7 +2957,7 @@ function factoryInterestsPath(paths) {
2662
2957
  }
2663
2958
  function readDoc(file) {
2664
2959
  try {
2665
- const raw = JSON.parse(fs10.readFileSync(file, "utf8"));
2960
+ const raw = JSON.parse(fs11.readFileSync(file, "utf8"));
2666
2961
  if (!Array.isArray(raw.interests)) return null;
2667
2962
  return { ...raw, interests: raw.interests.map(String) };
2668
2963
  } catch {
@@ -2680,8 +2975,8 @@ function ensureUserLayer(guard, paths) {
2680
2975
  return doc;
2681
2976
  }
2682
2977
  function saveDoc(guard, file, doc) {
2683
- fs10.mkdirSync(path10.dirname(file), { recursive: true });
2684
- fs10.writeFileSync(guard.assert(file), JSON.stringify(doc, null, 2), "utf8");
2978
+ fs11.mkdirSync(path10.dirname(file), { recursive: true });
2979
+ fs11.writeFileSync(guard.assert(file), JSON.stringify(doc, null, 2), "utf8");
2685
2980
  }
2686
2981
  function normalizeInterest(text) {
2687
2982
  return text.trim().replace(/\s+/g, " ");
@@ -2747,7 +3042,7 @@ function setWanderWindows(guard, paths, rawWindows) {
2747
3042
  }
2748
3043
 
2749
3044
  // src/statusbar/time-inject.ts
2750
- import fs11 from "fs";
3045
+ import fs12 from "fs";
2751
3046
  import path11 from "path";
2752
3047
  function shouldInjectTime(input) {
2753
3048
  if (input.step !== 1) return false;
@@ -2814,7 +3109,7 @@ function timeInjectStatePath(dataDir) {
2814
3109
  }
2815
3110
  function loadTimeInjectState(guard, dataDir) {
2816
3111
  try {
2817
- const parsed = JSON.parse(fs11.readFileSync(guard.assert(timeInjectStatePath(dataDir)), "utf8"));
3112
+ const parsed = JSON.parse(fs12.readFileSync(guard.assert(timeInjectStatePath(dataDir)), "utf8"));
2818
3113
  return parsed && typeof parsed === "object" ? parsed : {};
2819
3114
  } catch {
2820
3115
  return {};
@@ -2879,7 +3174,7 @@ var cachedVersion = null;
2879
3174
  function pluginVersion(paths) {
2880
3175
  if (cachedVersion) return cachedVersion;
2881
3176
  try {
2882
- const pkg = JSON.parse(fs12.readFileSync(path12.join(paths.packageRoot, "package.json"), "utf8"));
3177
+ const pkg = JSON.parse(fs13.readFileSync(path12.join(paths.packageRoot, "package.json"), "utf8"));
2883
3178
  cachedVersion = typeof pkg.version === "string" ? pkg.version : "unknown";
2884
3179
  } catch {
2885
3180
  cachedVersion = "unknown";
@@ -2911,12 +3206,12 @@ function loadSessionTitles() {
2911
3206
  };
2912
3207
  try {
2913
3208
  const dir = path12.join(os.homedir(), ".dsh", "storages", "session_projcache", "sessions");
2914
- for (const file of fs12.readdirSync(dir)) {
3209
+ for (const file of fs13.readdirSync(dir)) {
2915
3210
  if (!file.endsWith(".json")) continue;
2916
3211
  const id = file.slice(0, -".json".length);
2917
3212
  if (!id.startsWith("session-")) continue;
2918
3213
  try {
2919
- const record = JSON.parse(fs12.readFileSync(path12.join(dir, file), "utf8"));
3214
+ const record = JSON.parse(fs13.readFileSync(path12.join(dir, file), "utf8"));
2920
3215
  take(id, record.record);
2921
3216
  } catch {
2922
3217
  }
@@ -2924,7 +3219,7 @@ function loadSessionTitles() {
2924
3219
  } catch {
2925
3220
  }
2926
3221
  try {
2927
- const raw = JSON.parse(fs12.readFileSync(path12.join(os.homedir(), ".dsh", "storages", "session_projcache.json"), "utf8"));
3222
+ const raw = JSON.parse(fs13.readFileSync(path12.join(os.homedir(), ".dsh", "storages", "session_projcache.json"), "utf8"));
2928
3223
  const walk = (node) => {
2929
3224
  if (!node || typeof node !== "object") return;
2930
3225
  for (const [key, value] of Object.entries(node)) {
@@ -2940,6 +3235,27 @@ function loadSessionTitles() {
2940
3235
  return titles;
2941
3236
  }
2942
3237
  }
3238
+ function loadVisibleSessionIds(registryFile) {
3239
+ try {
3240
+ const file = registryFile ?? path12.join(os.homedir(), ".dsh", "storages", "workspace.json");
3241
+ const j = JSON.parse(fs13.readFileSync(file, "utf8"));
3242
+ const archived = new Set(
3243
+ (Array.isArray(j.global?.archivedSessionIds) ? j.global.archivedSessionIds : []).map((s) => String(s))
3244
+ );
3245
+ const isArchived = (id) => archived.has(id) || archived.has(id.replace(/^session-/, ""));
3246
+ const visible = /* @__PURE__ */ new Set();
3247
+ for (const ws of Object.values(j.tables?.workspaces ?? {})) {
3248
+ const ids = Array.isArray(ws?.sessionIds) ? ws.sessionIds : [];
3249
+ for (const raw of ids) {
3250
+ const id = String(raw);
3251
+ if (!isArchived(id)) visible.add(id);
3252
+ }
3253
+ }
3254
+ return { visible, registryFound: true };
3255
+ } catch {
3256
+ return { visible: /* @__PURE__ */ new Set(), registryFound: false };
3257
+ }
3258
+ }
2943
3259
  function installHeartbeatRpc(ctx, deps) {
2944
3260
  ctx.inject(["connection"], (scoped) => {
2945
3261
  const remoteCtx = scoped;
@@ -2997,10 +3313,12 @@ function installHeartbeatRpc(ctx, deps) {
2997
3313
  const bindings = loadBindings(guard, paths.settingsDir).bindings;
2998
3314
  const home = homeSessionId(paths, guard);
2999
3315
  const titles = loadSessionTitles();
3316
+ const { visible, registryFound } = loadVisibleSessionIds();
3000
3317
  const out = [];
3001
- if (fs12.existsSync(root)) {
3002
- for (const slug of fs12.readdirSync(root)) {
3003
- for (const id of fs12.readdirSync(path12.join(root, slug))) {
3318
+ if (fs13.existsSync(root)) {
3319
+ for (const slug of fs13.readdirSync(root)) {
3320
+ for (const id of fs13.readdirSync(path12.join(root, slug))) {
3321
+ if (registryFound && id !== home && !visible.has(id)) continue;
3004
3322
  const binding = bindings.find((b) => b.sessionId === id);
3005
3323
  out.push({
3006
3324
  id,
@@ -3035,7 +3353,7 @@ function installHeartbeatRpc(ctx, deps) {
3035
3353
  let homeReset = false;
3036
3354
  if (id === homeSessionId(paths, guard)) {
3037
3355
  try {
3038
- fs12.rmSync(guard.assert(path12.join(paths.dataDir, "gate.json")), { force: true });
3356
+ fs13.rmSync(guard.assert(path12.join(paths.dataDir, "gate.json")), { force: true });
3039
3357
  homeReset = true;
3040
3358
  } catch {
3041
3359
  }
@@ -3123,10 +3441,55 @@ function installHeartbeatRpc(ctx, deps) {
3123
3441
  }
3124
3442
  case "ledger.open": {
3125
3443
  const f = ledgerFilePath(paths.dataDir);
3126
- if (!fs12.existsSync(guard.assert(f))) fs12.writeFileSync(f, "# \u8D26\u672C\n", "utf8");
3444
+ if (!fs13.existsSync(guard.assert(f))) fs13.writeFileSync(f, "# \u8D26\u672C\n", "utf8");
3127
3445
  spawn2("cmd", ["/c", "start", "", f], { detached: true, stdio: "ignore" }).unref();
3128
3446
  return ok({ path: f });
3129
3447
  }
3448
+ case "weekly.list":
3449
+ return ok({ reports: listWeeklyReports(guard, paths.dataDir) });
3450
+ case "weekly.get": {
3451
+ const file = String(p.file ?? "");
3452
+ const rep = readWeeklyReport(guard, paths.dataDir, file);
3453
+ if (!rep) return err("bad-request", `no such report: ${file}`);
3454
+ return ok(rep);
3455
+ }
3456
+ case "migrate.export": {
3457
+ const passphrase = String(p.passphrase ?? "");
3458
+ if (!passphrase) return err("bad-request", "\u9700\u8981\u8BBE\u7F6E\u53E3\u4EE4");
3459
+ const { collectMigrationEntries, encryptContainer } = await import("./migrate-43PUK25G.js");
3460
+ const { entries } = collectMigrationEntries(guard, paths);
3461
+ if (entries.length === 0) return err("bad-request", "\u6CA1\u6709\u53EF\u6253\u5305\u7684\u8BB0\u5FC6\u6587\u4EF6\uFF08data/ \u662F\u7A7A\u7684\uFF09");
3462
+ const out = path12.join(paths.exportsDir, `heartbeat-memory-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.hbmig`);
3463
+ writeText(guard, out, encryptContainer(entries, passphrase));
3464
+ appendAuditLine(guard.assert(paths.logsDir + "/heartbeat.jsonl"), { event: "migrate_export", files: entries.length });
3465
+ return ok({ path: out, count: entries.length });
3466
+ }
3467
+ case "migrate.import": {
3468
+ const passphrase = String(p.passphrase ?? "");
3469
+ const file = String(p.file ?? "");
3470
+ if (!passphrase || !file) return err("bad-request", "\u9700\u8981\u5BB9\u5668\u8DEF\u5F84\u4E0E\u53E3\u4EE4");
3471
+ const { decryptContainer, applyMigrationEntries } = await import("./migrate-43PUK25G.js");
3472
+ let text;
3473
+ try {
3474
+ text = fs13.readFileSync(file, "utf8");
3475
+ } catch {
3476
+ return err("bad-request", `\u8BFB\u4E0D\u5230\u8FC1\u79FB\u5305\uFF1A${file}`);
3477
+ }
3478
+ let container;
3479
+ try {
3480
+ container = decryptContainer(text, passphrase);
3481
+ } catch (e) {
3482
+ return err("bad-request", String(e instanceof Error ? e.message : e));
3483
+ }
3484
+ const result = applyMigrationEntries(guard, paths, container.files);
3485
+ appendAuditLine(guard.assert(paths.logsDir + "/heartbeat.jsonl"), {
3486
+ event: "migrate_import",
3487
+ restored: result.restored.length,
3488
+ backedUp: result.backedUp.length,
3489
+ skipped: result.skipped.length
3490
+ });
3491
+ return ok(result);
3492
+ }
3130
3493
  default:
3131
3494
  return err("bad-request", `unknown endpoint ${JSON.stringify(endpoint)}`);
3132
3495
  }
@@ -3257,6 +3620,73 @@ function registerStatusbarSection(ctx, guard, paths, opts) {
3257
3620
  });
3258
3621
  }
3259
3622
 
3623
+ // src/ledger/tool.ts
3624
+ var MAX_TEXT = 120;
3625
+ function clean(v) {
3626
+ return String(v ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_TEXT);
3627
+ }
3628
+ function fmtEntry2(e) {
3629
+ return `#${e.id} [${e.status}] ${e.date} ${e.text}`;
3630
+ }
3631
+ function buildLedgerTool(guard, file) {
3632
+ return {
3633
+ name: "ledger",
3634
+ description: '\u5171\u4EAB\u8D26\u672C\u5DE5\u5177\uFF08\u5FC3\u8DF3\u63D2\u4EF6\u7684\u5F85\u529E\u4E0E\u4E8B\u9879\u8BB0\u5F55\uFF09\u3002\u5F53\u7528\u6237\u63D0\u5230\u8BA1\u5212\u3001\u5F85\u529E\u3001\u627F\u8BFA\u3001\u60F3\u505A\u7684\u4E8B\uFF0C\u6216\u8005\u67D0\u4EF6\u4E8B\u5DF2\u7ECF\u5B8C\u6210\u65F6\uFF0C\u4E3B\u52A8\u8C03\u7528\u672C\u5DE5\u5177\u767B\u8BB0\u6216\u52FE\u6389\u2014\u2014\u4E0D\u9700\u8981\u7B49\u7528\u6237\u660E\u786E\u8BF4"\u8BB0\u4E00\u4E0B"\u3002action=add \u767B\u8BB0\u65B0\u4E8B\u9879\uFF08text \u5FC5\u586B\uFF09\uFF1Baction=list \u67E5\u770B\u5F53\u524D\u672A\u5B8C\u6210\u4E8B\u9879\uFF1Baction=done \u52FE\u6389\u4E00\u6761\uFF08key \u4E3A #id \u6216\u6587\u672C\u5B50\u4E32\uFF09\u3002',
3635
+ parameters: {
3636
+ type: "object",
3637
+ properties: {
3638
+ action: { type: "string", enum: ["add", "list", "done"], description: "add=\u767B\u8BB0\uFF0Clist=\u67E5\u672A\u5B8C\u6210\uFF0Cdone=\u52FE\u6389" },
3639
+ text: { type: "string", description: "action=add \u65F6\u5FC5\u586B\uFF1A\u4E8B\u9879\u5185\u5BB9\uFF08\u4E00\u53E5\u8BDD\uFF09" },
3640
+ key: { type: "string", description: "action=done \u65F6\u5FC5\u586B\uFF1A#id \u6216\u4E8B\u9879\u6587\u672C\u7684\u552F\u4E00\u5B50\u4E32" }
3641
+ },
3642
+ required: ["action"],
3643
+ additionalProperties: false
3644
+ },
3645
+ output: {
3646
+ schema: {
3647
+ type: "object",
3648
+ properties: {
3649
+ ok: { type: "boolean" },
3650
+ message: { type: "string" }
3651
+ },
3652
+ required: ["ok", "message"],
3653
+ additionalProperties: false
3654
+ },
3655
+ render: (_args, value) => {
3656
+ const v = value;
3657
+ return [{ type: "text", text: String(v.message ?? "") }];
3658
+ }
3659
+ },
3660
+ timeoutMs: 5e3,
3661
+ // Ledger writes go through read-modify-write on one file; never parallel.
3662
+ isConcurrencySafe: () => false,
3663
+ async execute(args) {
3664
+ const a = args ?? {};
3665
+ const action = String(a.action ?? "");
3666
+ if (action === "add") {
3667
+ const text = clean(a.text);
3668
+ if (!text) return { ok: false, message: "add \u9700\u8981\u975E\u7A7A text" };
3669
+ const e = appendEntry(guard, file, text);
3670
+ return { ok: true, message: `\u5DF2\u767B\u8BB0 ${fmtEntry2(e)}` };
3671
+ }
3672
+ if (action === "done") {
3673
+ const key = clean(a.key);
3674
+ if (!key) return { ok: false, message: "done \u9700\u8981 key\uFF08#id \u6216\u6587\u672C\u5B50\u4E32\uFF09" };
3675
+ const e = markDone(guard, file, key);
3676
+ if (!e) return { ok: false, message: `\u6CA1\u6709\u627E\u5230\u5339\u914D\u300C${key}\u300D\u7684\u672A\u5B8C\u6210\u4E8B\u9879\uFF08\u5148\u7528 list \u67E5\uFF09` };
3677
+ return { ok: true, message: `\u5DF2\u5B8C\u6210 ${fmtEntry2(e)}` };
3678
+ }
3679
+ if (action === "list") {
3680
+ const items = scanPending(guard, file).map(fmtEntry2);
3681
+ return { ok: true, message: items.length > 0 ? `\u5F53\u524D\u672A\u5B8C\u6210 ${items.length} \u9879\uFF1A
3682
+ ${items.join("\n")}` : "\u8D26\u672C\u91CC\u6CA1\u6709\u672A\u5B8C\u6210\u4E8B\u9879" };
3683
+ }
3684
+ const total = readLedger(guard, file).entries.length;
3685
+ return { ok: false, message: `\u672A\u77E5 action\uFF0C\u8D26\u672C\u5171 ${total} \u6761` };
3686
+ }
3687
+ };
3688
+ }
3689
+
3260
3690
  // src/index.ts
3261
3691
  var name = "heartbeat";
3262
3692
  var inject = ["agents", "settings"];
@@ -3288,7 +3718,11 @@ var Config = Schema.object({
3288
3718
  /** v1.6.3 闲着模式:素材池为空时用画像话题兜底主动搭话(默认关)。 */
3289
3719
  idleMode: Schema.boolean().default(false),
3290
3720
  /** v1.7.0 节省 token 模式:用户离开(闲置 ≥30 分钟)或锁屏时整跳暂停(默认关)。 */
3291
- tokenSaver: Schema.boolean().default(false)
3721
+ tokenSaver: Schema.boolean().default(false),
3722
+ /** v1.8.0 账本工具:向所有日常会话 agent 注册共享账本工具(默认开)。 */
3723
+ ledgerTool: Schema.boolean().default(true),
3724
+ /** v1.8.0 引擎室追加工具:逗号分隔的全局工具名,存在才加入白名单(bili 压缩工具自动探测,无需手填)。 */
3725
+ extraTools: Schema.string().default("")
3292
3726
  });
3293
3727
  function apply(ctx, config = {}) {
3294
3728
  const paths = initWorkspace(config.dataDir ? { dataDir: config.dataDir } : {});
@@ -3307,7 +3741,14 @@ function apply(ctx, config = {}) {
3307
3741
  if (config.maxDailySend && config.maxDailySend >= 1) {
3308
3742
  policy = { ...policy, gate: { ...policy.gate, maxDailySend: config.maxDailySend } };
3309
3743
  }
3310
- const deps = { ctx, paths, guard, policy, agentPreset: config.agentPreset || "heartbeat" };
3744
+ const deps = {
3745
+ ctx,
3746
+ paths,
3747
+ guard,
3748
+ policy,
3749
+ agentPreset: config.agentPreset || "heartbeat",
3750
+ extraTools: (config.extraTools ?? "").split(",").map((s) => s.trim()).filter(Boolean)
3751
+ };
3311
3752
  setRuntime({
3312
3753
  paths,
3313
3754
  guard,
@@ -3436,6 +3877,24 @@ function apply(ctx, config = {}) {
3436
3877
  if (typeof merged.tokenSaver === "boolean") tokenSaverRef = merged.tokenSaver;
3437
3878
  };
3438
3879
  installHeartbeatRpc(ctx, { paths, guard, policy, ui: { get: uiGet, set: uiSet } });
3880
+ if (config.ledgerTool !== false) {
3881
+ ctx.inject(["tools"], (scoped) => {
3882
+ const audit = (entry) => {
3883
+ try {
3884
+ appendAuditLine(guard.assert(paths.logsDir + "/heartbeat.jsonl"), entry);
3885
+ } catch {
3886
+ }
3887
+ };
3888
+ try {
3889
+ const tools = scoped.tools;
3890
+ if (!tools || typeof tools.register !== "function") throw new Error("ToolRuntime.register unavailable on this host");
3891
+ tools.register(buildLedgerTool(guard, ledgerFilePath(paths.dataDir)));
3892
+ audit({ event: "ledger_tool_registered" });
3893
+ } catch (e) {
3894
+ audit({ event: "ledger_tool_register_failed", error: String(e).slice(0, 160) });
3895
+ }
3896
+ });
3897
+ }
3439
3898
  startOrchestrator(deps);
3440
3899
  const statusReader = new StatusReader();
3441
3900
  registerStatusbarSection(ctx, guard, paths, {