@kanadego/dsh-heartbeat 1.7.3 → 1.7.4
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 +8 -1
- package/dist/index.js +127 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -183,7 +183,8 @@ dsh plugin --profile web remove @kanadego/dsh-heartbeat
|
|
|
183
183
|
|
|
184
184
|
| 插件版本 | 适配的 DSH | 说明 |
|
|
185
185
|
|---|---|---|
|
|
186
|
-
| **v1.7.
|
|
186
|
+
| **v1.7.4**(当前) | ≥ `0.1.2-rc.1` | 修复引擎室会话无限增长撑爆上下文、心跳整体哑火的问题(自动轮换正身) |
|
|
187
|
+
| v1.7.3 | ≥ `0.1.2-rc.1` | 修复心跳进行中拖住宿主关闭/更新的问题 |
|
|
187
188
|
| v1.7.2 | ≥ `0.1.2-rc.1`(新增 `0.2.0-rc.1`) | 声明适配 `0.2.0-rc.1`,无行为改动 |
|
|
188
189
|
| v1.7.1 | ≥ `0.1.2-rc.1` | 修复 `0.1.7` 下「心跳模式」预设丢失:预设改为声明式注册,不再依赖文件系统用户根 |
|
|
189
190
|
| v1.7.0 | ≥ `0.1.2-rc.1`(在 `0.1.7-rc.2` 上验收) | 适配 0.1.7 的接口换代(设置接口 / 客户端服务 / 节律配置自持 / 会话消息源);新增 psy 分区开关与节省 token 模式 |
|
|
@@ -200,6 +201,12 @@ dsh plugin --profile web remove @kanadego/dsh-heartbeat
|
|
|
200
201
|
<details open>
|
|
201
202
|
<summary><b>v1.7.x</b></summary>
|
|
202
203
|
|
|
204
|
+
### v1.7.4 · 2026-09-30(修复:引擎室会话撑爆上下文,心跳哑火)
|
|
205
|
+
|
|
206
|
+
修复了引擎室会话上下文涨过模型的上下文窗口后导致**合并、反刍、投递全部失败**,且宿主的自动压缩也救不了的bug。现在心跳会**自动轮换引擎室会话**:事件数超阈值、或某轮因上下文溢出失败时,自动新建一个干净的引擎室会话接替,旧会话原样留在会话列表里(画像、账本、素材池都在插件数据目录,不受影响)。
|
|
207
|
+
|
|
208
|
+
另外:设置卡「会话绑定」的会话列表现在自动隐藏**已归档**和**子代理**会话。
|
|
209
|
+
|
|
203
210
|
### v1.7.3 · 2026-09-29(修复:心跳拖住 DSH 关闭)
|
|
204
211
|
|
|
205
212
|
修复心跳正在跑时 DSH 关不掉的问题——此前桌面端更新若恰逢一跳在跑,宿主等心跳结束等不到,更新会报「未能安全停止任务」。现在收到关闭信号,心跳会立刻停下手里这一跳,不再拖住宿主。
|
package/dist/index.js
CHANGED
|
@@ -1889,11 +1889,24 @@ function sessionEventCount(session) {
|
|
|
1889
1889
|
if (typeof session.seq === "number") return session.seq;
|
|
1890
1890
|
return sessionEvents(session).length;
|
|
1891
1891
|
}
|
|
1892
|
+
var HOME_ROTATE_EVENT_COUNT = 3e3;
|
|
1893
|
+
function isContextOverflowError(msg) {
|
|
1894
|
+
return /CONTEXT_WINDOW_EXCEEDED|context overflow/i.test(msg);
|
|
1895
|
+
}
|
|
1896
|
+
function shouldRotateHome(input) {
|
|
1897
|
+
return input.eventCount >= (input.threshold ?? HOME_ROTATE_EVENT_COUNT);
|
|
1898
|
+
}
|
|
1892
1899
|
var IDLE_WAIT_TIMEOUT_MS = 24e4;
|
|
1893
1900
|
var EXPRESSION_IDLE_WAIT_MS = 6e5;
|
|
1894
1901
|
var agentPromise = null;
|
|
1895
1902
|
var beating = false;
|
|
1896
1903
|
var beatCancel;
|
|
1904
|
+
var homeRotatePending = null;
|
|
1905
|
+
function markHomeRotateIfOverflow(error) {
|
|
1906
|
+
if (homeRotatePending === null && isContextOverflowError(String(error))) {
|
|
1907
|
+
homeRotatePending = `context overflow: ${String(error).slice(0, 120)}`;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1897
1910
|
var lastBeat = null;
|
|
1898
1911
|
function getLastBeat() {
|
|
1899
1912
|
return lastBeat;
|
|
@@ -1938,49 +1951,88 @@ function defaultAgentOptions(ctx) {
|
|
|
1938
1951
|
}
|
|
1939
1952
|
return void 0;
|
|
1940
1953
|
}
|
|
1954
|
+
function makeHomeSetup(deps) {
|
|
1955
|
+
const { ctx, paths } = deps;
|
|
1956
|
+
return async (agentCtx) => {
|
|
1957
|
+
const notes = [];
|
|
1958
|
+
const presetId = deps.agentPreset ?? "heartbeat";
|
|
1959
|
+
try {
|
|
1960
|
+
const presets = agentCtx.get("agentPresets");
|
|
1961
|
+
if (typeof presets?.mount !== "function") {
|
|
1962
|
+
notes.push("preset=no-api");
|
|
1963
|
+
} else {
|
|
1964
|
+
const preset = await presets.mount(agentCtx, presetId);
|
|
1965
|
+
const joined = preset?.id;
|
|
1966
|
+
notes.push(`preset=mounted(${joined ?? presetId})`);
|
|
1967
|
+
}
|
|
1968
|
+
} catch (e) {
|
|
1969
|
+
notes.push(`preset=threw(${String(e).slice(0, 200)})`);
|
|
1970
|
+
}
|
|
1971
|
+
const tools = agentCtx.get("tools");
|
|
1972
|
+
if (typeof tools?.restrict !== "function") {
|
|
1973
|
+
notes.push("restrict=no-api");
|
|
1974
|
+
} else {
|
|
1975
|
+
const allow = ["web_search"];
|
|
1976
|
+
try {
|
|
1977
|
+
tools.restrict({ allow });
|
|
1978
|
+
notes.push(`restrict=ok allow=${allow.join("|")}`);
|
|
1979
|
+
} catch (e) {
|
|
1980
|
+
notes.push(`restrict=threw(${String(e).slice(0, 200)})`);
|
|
1981
|
+
}
|
|
1982
|
+
try {
|
|
1983
|
+
const visible = (tools.schemas?.() ?? []).map((s) => String(s?.name ?? "?")).sort();
|
|
1984
|
+
notes.push(`visibleGlobal=${visible.length > 0 ? visible.join(",") : "(empty)"}`);
|
|
1985
|
+
} catch (e) {
|
|
1986
|
+
notes.push(`visibleGlobal=threw(${String(e).slice(0, 60)})`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
ctx.logger.info("heartbeat: tool policy %s", notes.join(" "));
|
|
1990
|
+
appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "tool_policy", policy: notes.join(" ") });
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
async function rotateHome(deps, reason) {
|
|
1994
|
+
const { ctx, guard, paths } = deps;
|
|
1995
|
+
const old = readBeatState(guard, paths).sessionId;
|
|
1996
|
+
const freshId = `session-${randomUUID2()}`;
|
|
1997
|
+
appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
|
|
1998
|
+
event: "home_rotate_start",
|
|
1999
|
+
from: old,
|
|
2000
|
+
to: freshId,
|
|
2001
|
+
reason
|
|
2002
|
+
});
|
|
2003
|
+
const handle = await withTimeout(
|
|
2004
|
+
Promise.resolve(ctx.agents.create({
|
|
2005
|
+
sessionId: freshId,
|
|
2006
|
+
meta: { cwd: paths.dataDir },
|
|
2007
|
+
...defaultAgentOptions(ctx) ? { agentOptions: defaultAgentOptions(ctx) } : {},
|
|
2008
|
+
setup: makeHomeSetup(deps)
|
|
2009
|
+
})),
|
|
2010
|
+
3e4,
|
|
2011
|
+
"home rotate create timeout (30s)"
|
|
2012
|
+
);
|
|
2013
|
+
const agent = unwrapHomeHandle(handle);
|
|
2014
|
+
const realId = agent.session?.id ?? freshId;
|
|
2015
|
+
writeBeatState(guard, paths, { sessionId: realId });
|
|
2016
|
+
agentPromise = null;
|
|
2017
|
+
appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
|
|
2018
|
+
event: "home_rotate_done",
|
|
2019
|
+
from: old,
|
|
2020
|
+
to: realId,
|
|
2021
|
+
reason
|
|
2022
|
+
});
|
|
2023
|
+
ctx.logger.info("heartbeat: home rotated %s -> %s (%s)", old ?? "(none)", realId, reason);
|
|
2024
|
+
}
|
|
2025
|
+
function unwrapHomeHandle(handle) {
|
|
2026
|
+
return handle.agent ?? handle;
|
|
2027
|
+
}
|
|
1941
2028
|
async function ensureAgent(deps) {
|
|
1942
2029
|
if (agentPromise) return agentPromise;
|
|
1943
2030
|
const { ctx, paths, guard } = deps;
|
|
1944
2031
|
const agentOptions = defaultAgentOptions(ctx);
|
|
2032
|
+
const setup = makeHomeSetup(deps);
|
|
1945
2033
|
agentPromise = (async () => {
|
|
1946
2034
|
const saved = readBeatState(guard, paths);
|
|
1947
2035
|
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
2036
|
const unwrap = (handle) => handle.agent ?? handle;
|
|
1985
2037
|
try {
|
|
1986
2038
|
let agent;
|
|
@@ -2112,6 +2164,7 @@ async function agentTurn(deps, agent, prompt, label, idleWaitMs = IDLE_WAIT_TIME
|
|
|
2112
2164
|
...turnError ? { turnError } : {},
|
|
2113
2165
|
window: shapes.slice(0, 12)
|
|
2114
2166
|
});
|
|
2167
|
+
if (turnError) markHomeRotateIfOverflow(turnError);
|
|
2115
2168
|
return "";
|
|
2116
2169
|
}
|
|
2117
2170
|
async function withTimeout(p, ms, label) {
|
|
@@ -2555,6 +2608,20 @@ async function beat(deps) {
|
|
|
2555
2608
|
}
|
|
2556
2609
|
};
|
|
2557
2610
|
}
|
|
2611
|
+
if (agent && homeRotatePending === null && shouldRotateHome({ eventCount: sessionEventCount(agent.session) })) {
|
|
2612
|
+
homeRotatePending = `size threshold (${sessionEventCount(agent.session)} events \u2265 ${HOME_ROTATE_EVENT_COUNT})`;
|
|
2613
|
+
}
|
|
2614
|
+
if (agent && homeRotatePending !== null) {
|
|
2615
|
+
try {
|
|
2616
|
+
await rotateHome(deps, homeRotatePending);
|
|
2617
|
+
homeRotatePending = null;
|
|
2618
|
+
} catch (e) {
|
|
2619
|
+
appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "home_rotate_failed", error: String(e).slice(0, 160) });
|
|
2620
|
+
}
|
|
2621
|
+
writeBeatStatus(deps, { beatStart, wandered: false });
|
|
2622
|
+
appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "silent", reason: "home rotated, next beat starts fresh" });
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2558
2625
|
let wandered = false;
|
|
2559
2626
|
if (agent) {
|
|
2560
2627
|
deferredRetries = 0;
|
|
@@ -2586,6 +2653,7 @@ async function beat(deps) {
|
|
|
2586
2653
|
writeBeatStatus(deps, { beatStart, wandered });
|
|
2587
2654
|
} catch (e) {
|
|
2588
2655
|
deps.ctx.logger.error("heartbeat: beat failed: %s", String(e).slice(0, 200));
|
|
2656
|
+
markHomeRotateIfOverflow(e);
|
|
2589
2657
|
try {
|
|
2590
2658
|
appendAuditLine(deps.paths.logsDir + "/heartbeat.jsonl", { event: "beat_error", error: String(e).slice(0, 200) });
|
|
2591
2659
|
noteBeat("error", { reason: String(e).slice(0, 120) });
|
|
@@ -2940,6 +3008,27 @@ function loadSessionTitles() {
|
|
|
2940
3008
|
return titles;
|
|
2941
3009
|
}
|
|
2942
3010
|
}
|
|
3011
|
+
function loadVisibleSessionIds(registryFile) {
|
|
3012
|
+
try {
|
|
3013
|
+
const file = registryFile ?? path12.join(os.homedir(), ".dsh", "storages", "workspace.json");
|
|
3014
|
+
const j = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
3015
|
+
const archived = new Set(
|
|
3016
|
+
(Array.isArray(j.global?.archivedSessionIds) ? j.global.archivedSessionIds : []).map((s) => String(s))
|
|
3017
|
+
);
|
|
3018
|
+
const isArchived = (id) => archived.has(id) || archived.has(id.replace(/^session-/, ""));
|
|
3019
|
+
const visible = /* @__PURE__ */ new Set();
|
|
3020
|
+
for (const ws of Object.values(j.tables?.workspaces ?? {})) {
|
|
3021
|
+
const ids = Array.isArray(ws?.sessionIds) ? ws.sessionIds : [];
|
|
3022
|
+
for (const raw of ids) {
|
|
3023
|
+
const id = String(raw);
|
|
3024
|
+
if (!isArchived(id)) visible.add(id);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
return { visible, registryFound: true };
|
|
3028
|
+
} catch {
|
|
3029
|
+
return { visible: /* @__PURE__ */ new Set(), registryFound: false };
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
2943
3032
|
function installHeartbeatRpc(ctx, deps) {
|
|
2944
3033
|
ctx.inject(["connection"], (scoped) => {
|
|
2945
3034
|
const remoteCtx = scoped;
|
|
@@ -2997,10 +3086,12 @@ function installHeartbeatRpc(ctx, deps) {
|
|
|
2997
3086
|
const bindings = loadBindings(guard, paths.settingsDir).bindings;
|
|
2998
3087
|
const home = homeSessionId(paths, guard);
|
|
2999
3088
|
const titles = loadSessionTitles();
|
|
3089
|
+
const { visible, registryFound } = loadVisibleSessionIds();
|
|
3000
3090
|
const out = [];
|
|
3001
3091
|
if (fs12.existsSync(root)) {
|
|
3002
3092
|
for (const slug of fs12.readdirSync(root)) {
|
|
3003
3093
|
for (const id of fs12.readdirSync(path12.join(root, slug))) {
|
|
3094
|
+
if (registryFound && id !== home && !visible.has(id)) continue;
|
|
3004
3095
|
const binding = bindings.find((b) => b.sessionId === id);
|
|
3005
3096
|
out.push({
|
|
3006
3097
|
id,
|