@kanadego/dsh-heartbeat 1.7.4 → 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;
@@ -1951,6 +2087,15 @@ function defaultAgentOptions(ctx) {
1951
2087
  }
1952
2088
  return void 0;
1953
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
+ }
1954
2099
  function makeHomeSetup(deps) {
1955
2100
  const { ctx, paths } = deps;
1956
2101
  return async (agentCtx) => {
@@ -1972,7 +2117,12 @@ function makeHomeSetup(deps) {
1972
2117
  if (typeof tools?.restrict !== "function") {
1973
2118
  notes.push("restrict=no-api");
1974
2119
  } else {
1975
- const allow = ["web_search"];
2120
+ let globalNames = [];
2121
+ try {
2122
+ globalNames = (tools.schemas?.() ?? []).map((s) => String(s?.name ?? ""));
2123
+ } catch {
2124
+ }
2125
+ const allow = buildEngineRoomAllowList(globalNames, deps.extraTools ?? []);
1976
2126
  try {
1977
2127
  tools.restrict({ allow });
1978
2128
  notes.push(`restrict=ok allow=${allow.join("|")}`);
@@ -1980,7 +2130,7 @@ function makeHomeSetup(deps) {
1980
2130
  notes.push(`restrict=threw(${String(e).slice(0, 200)})`);
1981
2131
  }
1982
2132
  try {
1983
- const visible = (tools.schemas?.() ?? []).map((s) => String(s?.name ?? "?")).sort();
2133
+ const visible = globalNames.sort();
1984
2134
  notes.push(`visibleGlobal=${visible.length > 0 ? visible.join(",") : "(empty)"}`);
1985
2135
  } catch (e) {
1986
2136
  notes.push(`visibleGlobal=threw(${String(e).slice(0, 60)})`);
@@ -2021,6 +2171,36 @@ async function rotateHome(deps, reason) {
2021
2171
  reason
2022
2172
  });
2023
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
+ }
2024
2204
  }
2025
2205
  function unwrapHomeHandle(handle) {
2026
2206
  return handle.agent ?? handle;
@@ -2196,6 +2376,7 @@ async function maintenancePhase(bc) {
2196
2376
  gcPool(guard, seedsFilePath(paths.dataDir), policy, now);
2197
2377
  pruneAuditFile(paths.logsDir + "/envpulse.jsonl", policy.retention.envPulseHours * 36e5, now);
2198
2378
  pruneAuditFile(paths.logsDir + "/heartbeat.jsonl", policy.retention.decisionLogDays * 864e5, now);
2379
+ snapshotIfDue(guard, paths.dataDir, paths.logsDir + "/heartbeat.jsonl", "maintenance-threshold", now);
2199
2380
  const cons = shouldConsolidate(guard, paths, policy, now);
2200
2381
  if (cons.due) {
2201
2382
  const llm = async (prompt) => {
@@ -2227,14 +2408,14 @@ async function observeBoundSessions(bc) {
2227
2408
  const { deps, now } = bc;
2228
2409
  const { guard, paths } = deps;
2229
2410
  const { loadBindings: loadBindings2, observeTargets } = await import("./bindings-XPPSKILN.js");
2230
- const { inboxAppend, inboxFilePath: inboxFilePath2 } = await import("./inbox-MMLHISQV.js");
2411
+ const { inboxAppend, inboxFilePath: inboxFilePath2 } = await import("./inbox-57NHQHY3.js");
2231
2412
  const data = loadBindings2(guard, paths.settingsDir);
2232
2413
  const targets = observeTargets(data);
2233
2414
  if (targets.length === 0) return;
2234
2415
  const cursorFile = path9.join(paths.dataDir, "cursors.json");
2235
2416
  let cursors = {};
2236
2417
  try {
2237
- cursors = JSON.parse(fs9.readFileSync(cursorFile, "utf8"));
2418
+ cursors = JSON.parse(fs10.readFileSync(cursorFile, "utf8"));
2238
2419
  } catch {
2239
2420
  }
2240
2421
  const inboxFile = inboxFilePath2(paths.dataDir);
@@ -2246,7 +2427,8 @@ async function observeBoundSessions(bc) {
2246
2427
  const cursor = cursors[b.sessionId] ?? 0;
2247
2428
  let last = cursor;
2248
2429
  let added = 0;
2249
- 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++) {
2250
2432
  const e = events[i];
2251
2433
  if (e.type !== "user/message") continue;
2252
2434
  const d = e.data;
@@ -2257,7 +2439,7 @@ async function observeBoundSessions(bc) {
2257
2439
  kind: "chat",
2258
2440
  at: new Date(now).toISOString(),
2259
2441
  ref: `cursors.json#${b.sessionId}:${i}`,
2260
- note: text.split(/[。!?\n]/)[0].slice(0, 80)
2442
+ note: text.split(/[。!?\n]/)[0].slice(0, maxChars)
2261
2443
  });
2262
2444
  added += 1;
2263
2445
  last = i + 1;
@@ -2272,6 +2454,50 @@ async function observeBoundSessions(bc) {
2272
2454
  }
2273
2455
  }
2274
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
+ }
2275
2501
  function ctx_getAgent(deps, sessionId) {
2276
2502
  try {
2277
2503
  const agent = deps.ctx.agents.get(sessionId);
@@ -2632,6 +2858,7 @@ async function beat(deps) {
2632
2858
  } else {
2633
2859
  await maintenancePhase(bc);
2634
2860
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "phase_done", phase: "maintenance" });
2861
+ await weeklyPhase(bc);
2635
2862
  await collectPhase(bc);
2636
2863
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "phase_done", phase: "collect" });
2637
2864
  wandered = await wanderPhase(bc);
@@ -2711,12 +2938,12 @@ function startOrchestrator(deps) {
2711
2938
 
2712
2939
  // src/rpc.ts
2713
2940
  import { spawn as spawn2 } from "child_process";
2714
- import fs12 from "fs";
2941
+ import fs13 from "fs";
2715
2942
  import os from "os";
2716
2943
  import path12 from "path";
2717
2944
 
2718
2945
  // src/browse/interests-edit.ts
2719
- import fs10 from "fs";
2946
+ import fs11 from "fs";
2720
2947
  import path10 from "path";
2721
2948
  var MAX_INTERESTS = 32;
2722
2949
  var MAX_INTEREST_LEN = 60;
@@ -2730,7 +2957,7 @@ function factoryInterestsPath(paths) {
2730
2957
  }
2731
2958
  function readDoc(file) {
2732
2959
  try {
2733
- const raw = JSON.parse(fs10.readFileSync(file, "utf8"));
2960
+ const raw = JSON.parse(fs11.readFileSync(file, "utf8"));
2734
2961
  if (!Array.isArray(raw.interests)) return null;
2735
2962
  return { ...raw, interests: raw.interests.map(String) };
2736
2963
  } catch {
@@ -2748,8 +2975,8 @@ function ensureUserLayer(guard, paths) {
2748
2975
  return doc;
2749
2976
  }
2750
2977
  function saveDoc(guard, file, doc) {
2751
- fs10.mkdirSync(path10.dirname(file), { recursive: true });
2752
- 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");
2753
2980
  }
2754
2981
  function normalizeInterest(text) {
2755
2982
  return text.trim().replace(/\s+/g, " ");
@@ -2815,7 +3042,7 @@ function setWanderWindows(guard, paths, rawWindows) {
2815
3042
  }
2816
3043
 
2817
3044
  // src/statusbar/time-inject.ts
2818
- import fs11 from "fs";
3045
+ import fs12 from "fs";
2819
3046
  import path11 from "path";
2820
3047
  function shouldInjectTime(input) {
2821
3048
  if (input.step !== 1) return false;
@@ -2882,7 +3109,7 @@ function timeInjectStatePath(dataDir) {
2882
3109
  }
2883
3110
  function loadTimeInjectState(guard, dataDir) {
2884
3111
  try {
2885
- const parsed = JSON.parse(fs11.readFileSync(guard.assert(timeInjectStatePath(dataDir)), "utf8"));
3112
+ const parsed = JSON.parse(fs12.readFileSync(guard.assert(timeInjectStatePath(dataDir)), "utf8"));
2886
3113
  return parsed && typeof parsed === "object" ? parsed : {};
2887
3114
  } catch {
2888
3115
  return {};
@@ -2947,7 +3174,7 @@ var cachedVersion = null;
2947
3174
  function pluginVersion(paths) {
2948
3175
  if (cachedVersion) return cachedVersion;
2949
3176
  try {
2950
- 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"));
2951
3178
  cachedVersion = typeof pkg.version === "string" ? pkg.version : "unknown";
2952
3179
  } catch {
2953
3180
  cachedVersion = "unknown";
@@ -2979,12 +3206,12 @@ function loadSessionTitles() {
2979
3206
  };
2980
3207
  try {
2981
3208
  const dir = path12.join(os.homedir(), ".dsh", "storages", "session_projcache", "sessions");
2982
- for (const file of fs12.readdirSync(dir)) {
3209
+ for (const file of fs13.readdirSync(dir)) {
2983
3210
  if (!file.endsWith(".json")) continue;
2984
3211
  const id = file.slice(0, -".json".length);
2985
3212
  if (!id.startsWith("session-")) continue;
2986
3213
  try {
2987
- const record = JSON.parse(fs12.readFileSync(path12.join(dir, file), "utf8"));
3214
+ const record = JSON.parse(fs13.readFileSync(path12.join(dir, file), "utf8"));
2988
3215
  take(id, record.record);
2989
3216
  } catch {
2990
3217
  }
@@ -2992,7 +3219,7 @@ function loadSessionTitles() {
2992
3219
  } catch {
2993
3220
  }
2994
3221
  try {
2995
- 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"));
2996
3223
  const walk = (node) => {
2997
3224
  if (!node || typeof node !== "object") return;
2998
3225
  for (const [key, value] of Object.entries(node)) {
@@ -3011,7 +3238,7 @@ function loadSessionTitles() {
3011
3238
  function loadVisibleSessionIds(registryFile) {
3012
3239
  try {
3013
3240
  const file = registryFile ?? path12.join(os.homedir(), ".dsh", "storages", "workspace.json");
3014
- const j = JSON.parse(fs12.readFileSync(file, "utf8"));
3241
+ const j = JSON.parse(fs13.readFileSync(file, "utf8"));
3015
3242
  const archived = new Set(
3016
3243
  (Array.isArray(j.global?.archivedSessionIds) ? j.global.archivedSessionIds : []).map((s) => String(s))
3017
3244
  );
@@ -3088,9 +3315,9 @@ function installHeartbeatRpc(ctx, deps) {
3088
3315
  const titles = loadSessionTitles();
3089
3316
  const { visible, registryFound } = loadVisibleSessionIds();
3090
3317
  const out = [];
3091
- if (fs12.existsSync(root)) {
3092
- for (const slug of fs12.readdirSync(root)) {
3093
- 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))) {
3094
3321
  if (registryFound && id !== home && !visible.has(id)) continue;
3095
3322
  const binding = bindings.find((b) => b.sessionId === id);
3096
3323
  out.push({
@@ -3126,7 +3353,7 @@ function installHeartbeatRpc(ctx, deps) {
3126
3353
  let homeReset = false;
3127
3354
  if (id === homeSessionId(paths, guard)) {
3128
3355
  try {
3129
- 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 });
3130
3357
  homeReset = true;
3131
3358
  } catch {
3132
3359
  }
@@ -3214,10 +3441,55 @@ function installHeartbeatRpc(ctx, deps) {
3214
3441
  }
3215
3442
  case "ledger.open": {
3216
3443
  const f = ledgerFilePath(paths.dataDir);
3217
- 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");
3218
3445
  spawn2("cmd", ["/c", "start", "", f], { detached: true, stdio: "ignore" }).unref();
3219
3446
  return ok({ path: f });
3220
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
+ }
3221
3493
  default:
3222
3494
  return err("bad-request", `unknown endpoint ${JSON.stringify(endpoint)}`);
3223
3495
  }
@@ -3348,6 +3620,73 @@ function registerStatusbarSection(ctx, guard, paths, opts) {
3348
3620
  });
3349
3621
  }
3350
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
+
3351
3690
  // src/index.ts
3352
3691
  var name = "heartbeat";
3353
3692
  var inject = ["agents", "settings"];
@@ -3379,7 +3718,11 @@ var Config = Schema.object({
3379
3718
  /** v1.6.3 闲着模式:素材池为空时用画像话题兜底主动搭话(默认关)。 */
3380
3719
  idleMode: Schema.boolean().default(false),
3381
3720
  /** v1.7.0 节省 token 模式:用户离开(闲置 ≥30 分钟)或锁屏时整跳暂停(默认关)。 */
3382
- 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("")
3383
3726
  });
3384
3727
  function apply(ctx, config = {}) {
3385
3728
  const paths = initWorkspace(config.dataDir ? { dataDir: config.dataDir } : {});
@@ -3398,7 +3741,14 @@ function apply(ctx, config = {}) {
3398
3741
  if (config.maxDailySend && config.maxDailySend >= 1) {
3399
3742
  policy = { ...policy, gate: { ...policy.gate, maxDailySend: config.maxDailySend } };
3400
3743
  }
3401
- 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
+ };
3402
3752
  setRuntime({
3403
3753
  paths,
3404
3754
  guard,
@@ -3527,6 +3877,24 @@ function apply(ctx, config = {}) {
3527
3877
  if (typeof merged.tokenSaver === "boolean") tokenSaverRef = merged.tokenSaver;
3528
3878
  };
3529
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
+ }
3530
3898
  startOrchestrator(deps);
3531
3899
  const statusReader = new StatusReader();
3532
3900
  registerStatusbarSection(ctx, guard, paths, {