@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/README.md +33 -12
- package/client.js +93 -0
- package/config/policy.json +9 -1
- package/dist/{chunk-4UE74TUB.js → chunk-63FUJLIF.js} +2 -2
- package/dist/{chunk-LLD7LUNN.js → chunk-IFTFDHZX.js} +2 -1
- package/dist/{chunk-SVP2NDRF.js → chunk-MXJPX3FM.js} +2 -2
- package/dist/chunk-RB3NLLGF.js +87 -0
- package/dist/chunk-RB3NLLGF.js.map +1 -0
- package/dist/{chunk-ON4MSU6E.js → chunk-SQJLKP7L.js} +175 -477
- package/dist/chunk-SQJLKP7L.js.map +1 -0
- package/dist/chunk-UCJR6TC7.js +163 -0
- package/dist/chunk-UCJR6TC7.js.map +1 -0
- package/dist/chunk-VIZNIQLK.js +472 -0
- package/dist/chunk-VIZNIQLK.js.map +1 -0
- package/dist/cli/index.js +138 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/{inbox-MMLHISQV.js → inbox-57NHQHY3.js} +3 -3
- package/dist/index.js +416 -48
- package/dist/index.js.map +1 -1
- package/dist/migrate-43PUK25G.js +20 -0
- package/dist/{pool-YAIBE2L5.js → pool-JQC5JWIZ.js} +3 -3
- package/dist/pool-JQC5JWIZ.js.map +1 -0
- package/dist/snapshot-ZBCSX67Y.js +15 -0
- package/dist/snapshot-ZBCSX67Y.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-ON4MSU6E.js.map +0 -1
- /package/dist/{chunk-4UE74TUB.js.map → chunk-63FUJLIF.js.map} +0 -0
- /package/dist/{chunk-LLD7LUNN.js.map → chunk-IFTFDHZX.js.map} +0 -0
- /package/dist/{chunk-SVP2NDRF.js.map → chunk-MXJPX3FM.js.map} +0 -0
- /package/dist/{inbox-MMLHISQV.js.map → inbox-57NHQHY3.js.map} +0 -0
- /package/dist/{pool-YAIBE2L5.js.map → migrate-43PUK25G.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
adviseRefillWander,
|
|
3
3
|
adviseWander,
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
16
|
-
loadProfileSchema,
|
|
15
|
+
markDone,
|
|
17
16
|
pendingOlderThan,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
readLedger,
|
|
18
|
+
readWeeklyReport,
|
|
19
|
+
renderTemplateReport,
|
|
20
|
+
saveWeeklyReport,
|
|
22
21
|
scanPending,
|
|
23
22
|
sendNewMessageHint,
|
|
23
|
+
sendWeeklyReadyHint,
|
|
24
24
|
updateUserPolicy,
|
|
25
|
-
userPresetRoot
|
|
26
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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, ...
|
|
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(
|
|
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-
|
|
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
|
-
|
|
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 =
|
|
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-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
2752
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
3092
|
-
for (const slug of
|
|
3093
|
-
for (const id of
|
|
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
|
-
|
|
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 (!
|
|
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 = {
|
|
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, {
|