@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/README.md +39 -11
- 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 +540 -81
- 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;
|
|
@@ -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-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
2684
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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 (
|
|
3002
|
-
for (const slug of
|
|
3003
|
-
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))) {
|
|
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
|
-
|
|
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 (!
|
|
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 = {
|
|
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, {
|