@kanadego/dsh-heartbeat 1.9.2 → 1.9.3

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/client.js CHANGED
@@ -199,11 +199,12 @@ window.__ModuleLoader__.load({
199
199
  );
200
200
  }
201
201
 
202
- // ── 素材池 ────────────────────────────────────────────────────
202
+ // ── 素材池(v1.9.3 三版):话题 / 聊天 × 活跃 / 归档,四个按钮各看一类 ──
203
+ // 和画像分区同一套 tab 交互,风格统一;一次只看一类。
203
204
  function SeedsSection() {
204
205
  const [data, setData] = React.useState(null);
205
206
  const [error, setError] = React.useState(null);
206
- const [showArchived, setShowArchived] = React.useState(false);
207
+ const [tab, setTab] = React.useState("topic");
207
208
  const [confirmId, setConfirmId] = React.useState(null);
208
209
  const reload = React.useCallback(() => {
209
210
  rpc("seeds.list").then(setData, (e) => setError(String(e).slice(0, 100)));
@@ -213,8 +214,8 @@ window.__ModuleLoader__.load({
213
214
  if (!data) return React.createElement("div", { style: hintStyle }, "加载中…");
214
215
  const act = (endpoint, id) => rpc(endpoint, { id }).then(reload, (e) => setError(String(e).slice(0, 100)));
215
216
  const row = (s, archived) => React.createElement("div", { key: s.id, style: rowList },
216
- React.createElement("span", { style: { flex: 1, fontSize: 12 } },
217
- "[", s.tag, "/", s.source, "] ", s.text.slice(0, 42), " used:", s.used),
217
+ React.createElement("span", { style: { flex: 1, fontSize: 12, wordBreak: "break-word" } },
218
+ "[", s.tag, "/", s.source, "] ", s.text.slice(0, 60), " used:", s.used),
218
219
  archived
219
220
  ? React.createElement("button", { style: buttonGhost, onClick: () => act("seeds.restore", s.id) }, "恢复")
220
221
  : React.createElement("button", { style: buttonGhost, onClick: () => act("seeds.archive", s.id) }, "归档"),
@@ -222,15 +223,34 @@ window.__ModuleLoader__.load({
222
223
  ? React.createElement("button", { style: { ...buttonStyle, background: "#e5484d" }, onClick: () => { setConfirmId(null); act("seeds.delete", s.id); } }, "确认删除")
223
224
  : React.createElement("button", { style: buttonGhost, onClick: () => setConfirmId(s.id) }, "删除"),
224
225
  );
226
+ const isChat = (s) => (s.category || "topic") === "chat";
227
+ const topics = data.active.filter((s) => !isChat(s));
228
+ const chats = data.active.filter(isChat);
229
+ const arcTopics = data.archived.filter((s) => !isChat(s));
230
+ const arcChats = data.archived.filter(isChat);
231
+ const archived = tab.indexOf("arc") === 0;
232
+ const chat = tab.indexOf("chat") >= 0;
233
+ const list = archived ? (chat ? arcChats : arcTopics) : (chat ? chats : topics);
234
+ const tabButton = (key, label, count) => React.createElement("button", {
235
+ key,
236
+ style: tab === key ? buttonStyle : buttonGhost,
237
+ onClick: () => setTab(key),
238
+ }, label + "(" + count + ")");
225
239
  return React.createElement(
226
240
  "div",
227
241
  null,
228
- React.createElement("div", { style: hintStyle }, "活跃 ", data.active.length, " / ", data.cap, " 条"),
229
- data.active.map((s) => row(s, false)),
230
- data.active.length === 0 ? React.createElement("div", { style: hintStyle }, "(池子是空的——浏览流和画像会慢慢喂养它)") : null,
231
- React.createElement("div", { style: rowStyle },
232
- React.createElement("button", { style: buttonGhost, onClick: () => setShowArchived(!showArchived) }, showArchived ? "收起归档区" : "归档区(" + data.archived.length + ")")),
233
- showArchived ? data.archived.map((s) => row(s, true)) : null,
242
+ React.createElement("div", { style: hintStyle }, "活跃 ", data.active.length, " / ", data.cap, " 条;归档 ", data.archived.length, " 条"),
243
+ React.createElement("div", { style: tabRow },
244
+ tabButton("topic", "话题素材", topics.length),
245
+ tabButton("chat", "聊天素材", chats.length),
246
+ tabButton("arc-topic", "话题·归档", arcTopics.length),
247
+ tabButton("arc-chat", "聊天·归档", arcChats.length)),
248
+ React.createElement("div", { style: hintStyle }, archived
249
+ ? (chat ? "归档的聊天素材:不再被挑中,可以恢复" : "归档的话题素材:不再被挑中,可以恢复")
250
+ : (chat ? "对话里长出来的:说过一次就算用掉" : "浏览流、手写、画像喂进来的:随机挑,可重复用")),
251
+ list.length === 0
252
+ ? React.createElement("div", { style: hintStyle }, archived ? "(这类还没有归档条目)" : "(这类还没有条目)")
253
+ : React.createElement("div", { style: scrollBox }, list.map((s) => row(s, archived))),
234
254
  );
235
255
  }
236
256
 
@@ -303,29 +323,85 @@ window.__ModuleLoader__.load({
303
323
  );
304
324
  }
305
325
 
306
- // ── 画像入口 ──────────────────────────────────────────────────
326
+ // ── 画像入口(v2.0):四个分区 + 引擎室输入 ─────────────────────
327
+ // 一次只看一个分区,条目一条一块、正文自动换行;另给一个只读的
328
+ // 「引擎室输入」视图,直接回答「截断后引擎室到底看到了什么」。
329
+ const PARTITIONS = [
330
+ { key: "interest", label: "兴趣" },
331
+ { key: "projects", label: "进行中" },
332
+ { key: "comm", label: "沟通偏好" },
333
+ { key: "psy", label: "心理" },
334
+ ];
335
+ const entryBox = { borderLeft: "2px solid var(--dsw-alias-border-l2)", paddingLeft: 8, margin: "6px 0" };
336
+ // v1.9.3:分区条目一多就顶到屏幕外,给个固定高度的滚动区。
337
+ const scrollBox = { maxHeight: 320, overflowY: "auto", padding: "4px 6px", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: 6 };
338
+ const groupHead = { fontSize: 12.5, fontWeight: 600, margin: "10px 0 2px" };
339
+ const entryHead = { fontSize: 12, color: "var(--dsw-alias-label-tertiary)", marginBottom: 2, wordBreak: "break-word" };
340
+ const entryBody = { fontSize: 12.5, lineHeight: 1.65, whiteSpace: "pre-wrap", wordBreak: "break-word", color: "var(--dsw-alias-label-secondary)", margin: "2px 0" };
341
+ const tabRow = { display: "flex", flexWrap: "wrap", gap: 6, margin: "6px 0" };
307
342
  function ProfileSection() {
308
343
  const [data, setData] = React.useState(null);
344
+ const [digest, setDigest] = React.useState(null);
345
+ const [tab, setTab] = React.useState("interest");
309
346
  const [error, setError] = React.useState(null);
347
+ const [status, setStatus] = React.useState("");
310
348
  const load = React.useCallback(() => {
311
- rpc("profile.digest").then(setData, (e) => setError(String(e).slice(0, 100)));
349
+ rpc("profile.partitions").then(setData, (e) => setError(String(e).slice(0, 120)));
350
+ rpc("profile.digest").then(setDigest, () => { /* 引擎室输入是附加视图,失败不挡分区 */ });
312
351
  }, []);
313
352
  React.useEffect(() => { load(); }, [load]);
314
353
  const doExport = () => rpc("profile.export").then(
315
- (v) => setData((d) => ({ ...d, exported: v.path })),
316
- (e) => setError(String(e).slice(0, 100)),
354
+ (v) => setStatus("已导出:" + v.path),
355
+ (e) => setError(String(e).slice(0, 120)),
317
356
  );
318
357
  if (error) return React.createElement("div", { style: hintStyle }, "加载失败:" + error);
319
358
  if (!data) return React.createElement("div", { style: hintStyle }, "加载中…");
359
+ const groups = data.partitions || {};
360
+ const group = groups[tab] || { total: 0, valid: 0, entries: [] };
361
+ const entries = group.entries || [];
362
+ const tabButton = (key, label, count) => React.createElement("button", {
363
+ key,
364
+ style: tab === key ? buttonStyle : buttonGhost,
365
+ onClick: () => setTab(key),
366
+ }, count === undefined ? label : label + "(" + count + ")");
367
+ const om = (digest && digest.omitted) || { tact: 0, topic: 0, wander: 0 };
320
368
  return React.createElement(
321
369
  "div",
322
370
  { style: listStyle },
323
- React.createElement("div", null, React.createElement("b", null, "处境切面"), React.createElement("pre", { style: { whiteSpace: "pre-wrap", margin: "2px 0", fontSize: 12 } }, data.tact || "(空)")),
324
- React.createElement("div", null, React.createElement("b", null, "话题切面"), React.createElement("pre", { style: { whiteSpace: "pre-wrap", margin: "2px 0", fontSize: 12 } }, data.topic || "(空)")),
371
+ React.createElement("div", { style: tabRow },
372
+ PARTITIONS.map((p) => tabButton(p.key, p.label, (groups[p.key] || {}).valid || 0)),
373
+ tabButton("__engine", "引擎室输入")),
374
+ tab === "__engine"
375
+ ? React.createElement("div", null,
376
+ React.createElement("div", { style: hintStyle }, digest
377
+ ? "共 " + digest.totalChars + " / " + digest.budgetChars + " 字符(" + (digest.withinBudget ? "在预算内" : "超预算") + ");按预算省略:处境 " + om.tact + " 条、话题 " + om.topic + " 条、兴趣 " + om.wander + " 条"
378
+ : "加载中…"),
379
+ React.createElement("div", { style: scrollBox },
380
+ React.createElement("div", null,
381
+ React.createElement("b", null, "处境切面"),
382
+ React.createElement("div", { style: entryBody }, (digest && digest.tact) || "(空)")),
383
+ React.createElement("div", null,
384
+ React.createElement("b", null, "话题切面"),
385
+ React.createElement("div", { style: entryBody }, (digest && digest.topic) || "(空)")),
386
+ React.createElement("div", null,
387
+ React.createElement("b", null, "兴趣切面"),
388
+ React.createElement("div", { style: entryBody }, (digest && digest.wander) || "(空)"))))
389
+ : React.createElement("div", null,
390
+ React.createElement("div", { style: hintStyle }, "有效 " + group.valid + " 条 / 历史 " + (group.total - group.valid) + " 条(过期条目不进引擎室输入,也不在这里列出)"),
391
+ entries.length === 0
392
+ ? React.createElement("div", { style: hintStyle }, "(这个分区还没有条目)")
393
+ : React.createElement("div", { style: scrollBox }, entries.map((e) => React.createElement("div", { key: e.id || (String(e.topic) + String(e.content).slice(0, 12)), style: entryBox },
394
+ React.createElement("div", { style: entryHead },
395
+ (e.topic || "") + "/" + (e.subTopic || "")
396
+ + " conf " + Number(e.confidence || 0).toFixed(2)
397
+ + (e.temporal ? " " + e.temporal : "")
398
+ + (e.lowActivity ? " 久未验证" : "")
399
+ + (e.updatedAt ? " 更新 " + String(e.updatedAt).slice(0, 10) : "")),
400
+ React.createElement("div", { style: entryBody }, e.content || ""))))),
325
401
  React.createElement("div", { style: rowStyle },
326
402
  React.createElement("button", { style: buttonGhost, onClick: () => { load(); } }, "刷新"),
327
403
  React.createElement("button", { style: buttonGhost, onClick: () => { void doExport(); } }, "导出 Markdown")),
328
- data.exported ? React.createElement("div", { style: hintStyle }, "已导出:" + data.exported) : null,
404
+ status ? React.createElement("div", { style: hintStyle }, status) : null,
329
405
  );
330
406
  }
331
407
 
@@ -339,10 +415,10 @@ window.__ModuleLoader__.load({
339
415
  React.useEffect(() => {
340
416
  rpc("config.get").then(setValue, (e) => setLoadErr(String(e).slice(0, 100)));
341
417
  }, []);
342
- const interval = value && Number(value.intervalMin) > 0 ? Number(value.intervalMin) : 20;
343
- const cap = value && Number(value.maxDailySend) > 0 ? Number(value.maxDailySend) : 3;
418
+ const interval = value && Number(value.intervalMin) > 0 ? Number(value.intervalMin) : 60;
419
+ const cap = value && Number(value.maxDailySend) > 0 ? Number(value.maxDailySend) : 10;
344
420
  const timeInject = value && value.timeInjectMin === 0 ? 0 : (value && Number(value.timeInjectMin) > 0 ? Number(value.timeInjectMin) : 25);
345
- const statusbar = !value || value.statusbar !== false;
421
+ const statusbar = !!value && value.statusbar === true;
346
422
  const idleMode = !!value && value.idleMode === true;
347
423
  const tokenSaver = !!value && value.tokenSaver === true;
348
424
  const psyEnabled = !!value && value.psyEnabled === true;
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "heartbeat": {
3
- "intervalMin": 20,
3
+ "intervalMin": 60,
4
4
  "idleMode": false,
5
5
  "archiveRotatedHome": true
6
6
  },
7
7
  "gate": {
8
- "maxDailySend": 3,
8
+ "maxDailySend": 10,
9
9
  "cooldownMinutes": 30,
10
10
  "quietHours": {
11
11
  "start": "01:00",
@@ -64,7 +64,8 @@
64
64
  "perBeat": 10
65
65
  },
66
66
  "weekly": {
67
- "enabled": true
67
+ "enabled": true,
68
+ "keepReports": 3
68
69
  },
69
70
  "retention": {
70
71
  "envPulseHours": 48,
@@ -10,6 +10,7 @@ import {
10
10
  minutesOfDay,
11
11
  pendingOlderThan,
12
12
  preferenceFilePath,
13
+ pruneWeeklyReports,
13
14
  readLedger,
14
15
  recordDelivery,
15
16
  renderTemplateReport,
@@ -20,7 +21,7 @@ import {
20
21
  sendNewMessageHint,
21
22
  sendWeeklyReadyHint,
22
23
  weeklyDue
23
- } from "./chunk-IG6YMUHW.js";
24
+ } from "./chunk-WLZSABBH.js";
24
25
  import {
25
26
  getRuntime
26
27
  } from "./chunk-3QJJRXIR.js";
@@ -560,7 +561,8 @@ function assembleCandidates(seeds, opts = {}) {
560
561
  if (topic.length < 4) chat.push(...chatPool.slice(2, 2 + (4 - topic.length)));
561
562
  return [...topic, ...chat].slice(0, 6);
562
563
  }
563
- var PACKAGE_DECLARE = "\u8FD9\u662F\u5FC3\u8DF3\u63D2\u4EF6\u7D20\u6750\u6295\u9012,\u8BF7\u4F60\u6839\u636E\u5F53\u524D\u5904\u5883\u5224\u65AD\u8981\u4E0D\u8981\u9009\u4E00\u6761\u8BF4";
564
+ var PACKAGE_DECLARE = "\u8FD9\u662F\u5FC3\u8DF3\u63D2\u4EF6\u7D20\u6750\u6295\u9012,\u8BF7\u4F60\u6839\u636E\u5F53\u524D\u5904\u5883\u5224\u65AD\u8981\u4E0D\u8981\u5F00\u53E3:\u8BE5\u8BF4\u5C31\u9009\u4E00\u6761\u8BF4,\u4E0D\u8BE5\u8BF4\u5C31\u7559\u767D";
565
+ var SILENCE_DECLARE = '(\u51B3\u5B9A\u4E0D\u8BF4\u7684\u65F6\u5019:\u56DE\u590D\u91CC\u4E0D\u8981\u5199\u4EFB\u4F55\u6B63\u6587,\u4E5F\u4E0D\u8981\u5199"\u55EF""\u5728\u7684"\u8FD9\u79CD\u5BA2\u5957,\u53EA\u8C03\u4E00\u6B21 seed_report \u62A5 spoken=silent \u5E76\u5199\u4E0A reason;\u7A7A\u7740\u5C31\u662F\u771F\u7684\u6C89\u9ED8)';
564
566
  function materialLines(materials) {
565
567
  return materials.map((m) => `- [${m.id}] ${m.text.trim()}`);
566
568
  }
@@ -578,13 +580,14 @@ function buildMaterialPrompt(materials, opts = {}) {
578
580
  if (wantHonestOption(materials, opts.threshold ?? 2)) {
579
581
  lines.push("(\u6216\u8005\u4E5F\u53EF\u4EE5\u8BF4\u4E00\u53E5\u771F\u5FC3\u8BDD,\u4E0D\u5E26\u7D20\u6750)");
580
582
  }
583
+ lines.push(SILENCE_DECLARE);
581
584
  if (opts.deliveryId) {
582
585
  lines.push(`(\u672C\u6B21\u6295\u9012\u7F16\u53F7:${opts.deliveryId},\u62A5\u8D26\u65F6\u539F\u6837\u586B\u8FDB delivery_id)`);
583
586
  }
584
587
  lines.push(REPORT_DECLARE);
585
588
  return lines.join("\n");
586
589
  }
587
- var REPORT_DECLARE = "\u6700\u540E,\u65E0\u8BBA\u521A\u624D\u8BF4\u4E0D\u8BF4\u8BDD\u3001\u7528\u6CA1\u7528\u7D20\u6750,\u90FD\u8981\u8C03\u7528\u4E00\u6B21 seed_report \u5DE5\u5177\u62A5\u8D26:spoken=material(\u8BF4\u4E86\u7D20\u6750)/heartfelt(\u8BF4\u4E86\u4E0D\u5E26\u7D20\u6750\u7684\u8BDD)/silent(\u6CA1\u8BF4\u8BDD);\u7528\u4E86\u7D20\u6750\u5C31\u628A\u7D20\u6750\u7F16\u53F7\u586B\u8FDB seed_ids(\u53EF\u591A\u6761,\u53EA\u586B\u7F16\u53F7\u672C\u8EAB);\u7528\u4E86\u753B\u50CF\u6761\u76EE\u5F53\u8BDD\u9898\u5C31\u586B profile_ids;\u6CA1\u7528\u7D20\u6750\u65F6\u7528 reason \u7B80\u5355\u8BB0\u4E00\u7B14\u539F\u56E0(\u4E0D\u60F3\u8BF4\u8BDD/\u7D20\u6750\u4E0D\u642D/\u5728\u5FD9\u6216\u521A\u804A\u8FC7,\u65E0\u9700\u7D20\u6750);\u5E76\u628A\u672C\u6B21\u6295\u9012\u7F16\u53F7\u586B\u8FDB delivery_id\u3002";
590
+ var REPORT_DECLARE = "\u6700\u540E,\u65E0\u8BBA\u521A\u624D\u8BF4\u4E0D\u8BF4\u8BDD\u3001\u7528\u6CA1\u7528\u7D20\u6750,\u90FD\u8981\u8C03\u7528\u4E00\u6B21 seed_report \u5DE5\u5177\u62A5\u8D26(\u4E0D\u8BF4\u8BDD\u65F6\u6B63\u6587\u5C31\u7559\u7A7A):spoken=material(\u8BF4\u4E86\u7D20\u6750)/heartfelt(\u8BF4\u4E86\u4E0D\u5E26\u7D20\u6750\u7684\u8BDD)/silent(\u6CA1\u8BF4\u8BDD);\u7528\u4E86\u7D20\u6750\u5C31\u628A\u7D20\u6750\u7F16\u53F7\u586B\u8FDB seed_ids(\u53EF\u591A\u6761,\u53EA\u586B\u7F16\u53F7\u672C\u8EAB);\u7528\u4E86\u753B\u50CF\u6761\u76EE\u5F53\u8BDD\u9898\u5C31\u586B profile_ids;\u6CA1\u7528\u7D20\u6750\u65F6\u7528 reason \u7B80\u5355\u8BB0\u4E00\u7B14\u539F\u56E0(\u4E0D\u60F3\u8BF4\u8BDD/\u7D20\u6750\u4E0D\u642D/\u5728\u5FD9\u6216\u521A\u804A\u8FC7,\u65E0\u9700\u7D20\u6750);\u5E76\u628A\u672C\u6B21\u6295\u9012\u7F16\u53F7\u586B\u8FDB delivery_id\u3002";
588
591
  function explicitIds(candidates, ids = []) {
589
592
  const known = new Set(candidates.map((c) => c.id));
590
593
  return [...new Set(ids)].filter((id) => known.has(id));
@@ -1100,7 +1103,11 @@ function summarizeRhythm(paths) {
1100
1103
  }
1101
1104
 
1102
1105
  // src/profile/digest.ts
1103
- var BUDGET_CHARS = 3200;
1106
+ var BUDGET_CHARS = 6e3;
1107
+ var TACT_MAX_CHARS = 2500;
1108
+ var WANDER_MAX_CHARS = 850;
1109
+ var TOPIC_MIN_CHARS = 2400;
1110
+ var TOPN_DEFAULT = 12;
1104
1111
  function fmtEntry(prefix, content, opts) {
1105
1112
  const flag = opts.low ? "\uFF08\u4E45\u672A\u9A8C\u8BC1\uFF09" : "";
1106
1113
  return `${prefix}${content}${flag} [conf ${opts.confidence.toFixed(2)}]`;
@@ -1109,9 +1116,25 @@ function profileTopicEntries(doc, topN) {
1109
1116
  const score = (e) => e.confidence * 0.7 + 1 / (1 + Math.max(0, Date.now() - Date.parse(e.updatedAt)) / 864e5) * 0.3;
1110
1117
  return [...doc.partitions.interest.entries, ...doc.partitions.projects.entries].filter((e) => e.validTo === null).sort((a, b) => score(b) - score(a)).slice(0, topN);
1111
1118
  }
1119
+ function capSection(entries, cap) {
1120
+ const kept = [];
1121
+ let len = 0;
1122
+ let omitted = 0;
1123
+ for (const line of entries) {
1124
+ const add = kept.length === 0 ? line.length : line.length + 1;
1125
+ if (len + add > cap) {
1126
+ omitted += 1;
1127
+ continue;
1128
+ }
1129
+ kept.push(line);
1130
+ len += add;
1131
+ }
1132
+ if (kept.length === 0 && omitted > 0) return { text: `\uFF08\u672C\u8282 ${omitted} \u6761\u8D85\u51FA\u9884\u7B97\uFF0C\u5DF2\u7701\u7565\uFF09`, omitted };
1133
+ return { text: kept.join("\n"), omitted };
1134
+ }
1112
1135
  function buildDigest(guard, paths, policy, input = {}) {
1113
1136
  const doc = loadProfile(guard, profileFilePath(paths.dataDir));
1114
- const topN = input.topN ?? 8;
1137
+ const topN = input.topN ?? TOPN_DEFAULT;
1115
1138
  const summary = summarizeRhythm(paths);
1116
1139
  let rhythmLine = "\u4F5C\u606F\u672A\u77E5\uFF08\u6837\u672C\u4E0D\u8DB3\uFF09";
1117
1140
  if (summary.daysSampled > 0 && summary.peakHours.length > 0) {
@@ -1127,24 +1150,25 @@ function buildDigest(guard, paths, policy, input = {}) {
1127
1150
  return fmtEntry(prefix, `${e.topic}/${e.subTopic}: ${e.content}`, { low: e.lowActivity, confidence: e.confidence });
1128
1151
  });
1129
1152
  const wanderEntries = doc.partitions.interest.entries.filter((e) => e.validTo === null && e.confidence >= 0.6 && e.subTopic === "preference").slice(0, 5).map((e) => `- ${e.content} [conf ${e.confidence.toFixed(2)}]`);
1130
- const stale = pendingOlderThan(guard, ledgerFilePath(paths.dataDir), 3).slice(0, 3).map((e) => `- ${e.text}\uFF08${e.date}\uFF09`);
1131
- const tact = tactParts.join("\n");
1132
- const topic = [
1133
- ...topicEntries,
1134
- ...stale.length ? ["[\u8D26\u672C\u5F85\u8DDF\u8FDB] ", ...stale] : []
1135
- ].join("\n");
1136
- const wander = wanderEntries.join("\n") || "(\u65E0\u9AD8\u7F6E\u4FE1\u5174\u8DA3)";
1137
- const totalChars = tact.length + topic.length + wander.length;
1138
- let outTopic = topic;
1139
- if (tact.length + outTopic.length + wander.length > BUDGET_CHARS && outTopic.length > 800) {
1140
- outTopic = outTopic.slice(0, 800) + "\n(\u5DF2\u622A\u65AD\u4EE5\u63A7\u5236\u9884\u7B97)";
1141
- }
1153
+ const ledgerBlock = pendingOlderThan(guard, ledgerFilePath(paths.dataDir), 3).slice(0, 3).map((e) => `- ${e.text}\uFF08${e.date}\uFF09`);
1154
+ const ledgerText = ledgerBlock.length ? ["[\u8D26\u672C\u5F85\u8DDF\u8FDB] ", ...ledgerBlock].join("\n") : "";
1155
+ const tact = capSection(tactParts, TACT_MAX_CHARS);
1156
+ const wander = capSection(wanderEntries.length ? wanderEntries : ["(\u65E0\u9AD8\u7F6E\u4FE1\u5174\u8DA3)"], WANDER_MAX_CHARS);
1157
+ const topicBudget = Math.max(
1158
+ TOPIC_MIN_CHARS,
1159
+ BUDGET_CHARS - tact.text.length - wander.text.length - ledgerText.length - 40
1160
+ );
1161
+ const topicCapped = capSection(topicEntries, topicBudget);
1162
+ const topic = [topicCapped.text, ledgerText].filter((s) => s.length > 0).join("\n");
1163
+ const totalChars = tact.text.length + topic.length + wander.text.length;
1142
1164
  return {
1143
- tact,
1144
- topic: outTopic,
1145
- wander,
1146
- totalChars: tact.length + outTopic.length + wander.length,
1147
- withinBudget: tact.length + outTopic.length + wander.length <= BUDGET_CHARS
1165
+ tact: tact.text,
1166
+ topic,
1167
+ wander: wander.text,
1168
+ totalChars,
1169
+ withinBudget: totalChars <= BUDGET_CHARS,
1170
+ omitted: { tact: tact.omitted, topic: topicCapped.omitted, wander: wander.omitted },
1171
+ budgetChars: BUDGET_CHARS
1148
1172
  };
1149
1173
  }
1150
1174
 
@@ -1869,6 +1893,10 @@ async function weeklyPhase(bc) {
1869
1893
  text
1870
1894
  });
1871
1895
  audit({ event: "weekly_generated", source, chars: text.length });
1896
+ const pruned = pruneWeeklyReports(guard, paths.dataDir, policy.weekly.keepReports);
1897
+ if (pruned.length > 0) {
1898
+ audit({ event: "weekly_pruned", keep: policy.weekly.keepReports, removed: pruned });
1899
+ }
1872
1900
  if (!inQuietHours(policy, now)) {
1873
1901
  try {
1874
1902
  await sendWeeklyReadyHint(paths);
@@ -2262,13 +2290,13 @@ function writeBeatStatus(deps, info) {
2262
2290
  const pulse = readPulse(guard, paths);
2263
2291
  const last = getLastBeat();
2264
2292
  const spokeThisBeat = last?.verdict === "spoke" && !!last.at && last.at >= info.beatStart;
2265
- const scene = deriveScene({
2293
+ const scene = info.away ? "away" : deriveScene({
2266
2294
  quietHours: inQuietHours(policy, Date.now()),
2267
2295
  spokeThisBeat,
2268
2296
  wanderedThisBeat: info.wandered,
2269
2297
  presence: pulse?.presence ?? "unknown"
2270
2298
  });
2271
- const note = last?.verdict === "spoke" ? clampNote(last.text) : void 0;
2299
+ const note = info.away ? "\u4F60\u4E0D\u5728\uFF0C\u5FC3\u8DF3\u6302\u8D77" : last?.verdict === "spoke" ? clampNote(last.text) : void 0;
2272
2300
  writeStatus(guard, paths, {
2273
2301
  at: (/* @__PURE__ */ new Date()).toISOString(),
2274
2302
  scene,
@@ -2291,12 +2319,72 @@ function scheduleDeferredRetry(deps) {
2291
2319
  }, delayMs);
2292
2320
  }
2293
2321
  var TOKEN_SAVER_IDLE_SECONDS = 1800;
2322
+ var TOKEN_SAVER_WAKE_MS = 3 * 6e4;
2323
+ var TOKEN_SAVER_ACTIVITY_SCAN = 200;
2324
+ var wakeTimer;
2325
+ var pausedSince = 0;
2326
+ var heldChecks = 0;
2327
+ function clearTokenSaverWake() {
2328
+ if (wakeTimer) {
2329
+ clearTimeout(wakeTimer);
2330
+ wakeTimer = void 0;
2331
+ }
2332
+ }
2333
+ function scheduleTokenSaverWake(deps) {
2334
+ if (!pausedSince) pausedSince = Date.now();
2335
+ if (wakeTimer) return;
2336
+ wakeTimer = setTimeout(() => {
2337
+ wakeTimer = void 0;
2338
+ void (async () => {
2339
+ try {
2340
+ if (await tokenSaverActive(deps)) {
2341
+ heldChecks += 1;
2342
+ scheduleTokenSaverWake(deps);
2343
+ return;
2344
+ }
2345
+ const heldMin = Math.round((Date.now() - pausedSince) / 6e4);
2346
+ const checks = heldChecks;
2347
+ clearTokenSaverWake();
2348
+ pausedSince = 0;
2349
+ heldChecks = 0;
2350
+ appendAuditLine(deps.paths.logsDir + "/heartbeat.jsonl", { event: "token_saver_resumed", via: "wake", heldMin, checks });
2351
+ void beat(deps);
2352
+ } catch {
2353
+ }
2354
+ })();
2355
+ }, TOKEN_SAVER_WAKE_MS);
2356
+ wakeTimer.unref?.();
2357
+ }
2294
2358
  async function tokenSaverActive(deps) {
2295
2359
  if (!getRuntime().flags.tokenSaver()) return false;
2360
+ if (await userActiveInBoundSessions(deps)) return false;
2296
2361
  if (await probeWorkstationLocked()) return true;
2297
2362
  const idle = await probeIdleSeconds(deps.guard, deps.paths);
2298
2363
  return idle >= TOKEN_SAVER_IDLE_SECONDS;
2299
2364
  }
2365
+ async function userActiveInBoundSessions(deps) {
2366
+ try {
2367
+ const { loadBindings, observeTargets } = await import("./bindings-225SI7KC.js");
2368
+ const targets = observeTargets(loadBindings(deps.guard, deps.paths.settingsDir));
2369
+ if (targets.length === 0) return false;
2370
+ const cursors = loadCursors(path9.join(deps.paths.dataDir, "cursors.json")).sessions;
2371
+ for (const b of targets) {
2372
+ const agent = ctx_getAgent(deps, b.sessionId);
2373
+ if (!agent) continue;
2374
+ const count = sessionEventCount(agent.session);
2375
+ const from = Math.max(cursors[b.sessionId] ?? 0, count - TOKEN_SAVER_ACTIVITY_SCAN);
2376
+ for (const e of sessionEvents(agent.session, from)) {
2377
+ if (e.type !== "user/message") continue;
2378
+ const d = e.data;
2379
+ if (d?.source && (d.source.kind === "heartbeat" || d.source.kind === "plugin" || d.source.plugin === "heartbeat")) continue;
2380
+ return true;
2381
+ }
2382
+ }
2383
+ return false;
2384
+ } catch {
2385
+ return false;
2386
+ }
2387
+ }
2300
2388
  async function beat(deps) {
2301
2389
  if (beating) return;
2302
2390
  beating = true;
@@ -2340,13 +2428,26 @@ async function beat(deps) {
2340
2428
  return;
2341
2429
  }
2342
2430
  let wandered = false;
2431
+ let pausedAway = false;
2343
2432
  if (agent) {
2344
2433
  deferredRetries = 0;
2345
2434
  const bc = { deps, agent, now };
2346
2435
  if (await tokenSaverActive(deps)) {
2347
2436
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "silent", reason: "token-saver" });
2348
2437
  noteBeat("silent", { reason: "token-saver\uFF08\u4F60\u4E0D\u5728\uFF0C\u5FC3\u8DF3\u6302\u8D77\uFF09" });
2438
+ pausedAway = true;
2439
+ scheduleTokenSaverWake(deps);
2349
2440
  } else {
2441
+ if (pausedSince) {
2442
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
2443
+ event: "token_saver_resumed",
2444
+ via: "interval",
2445
+ heldMin: Math.round((Date.now() - pausedSince) / 6e4)
2446
+ });
2447
+ }
2448
+ clearTokenSaverWake();
2449
+ pausedSince = 0;
2450
+ heldChecks = 0;
2350
2451
  await withTimeout(maintenancePhase(bc), MAINTENANCE_TIMEOUT_MS, "maintenance");
2351
2452
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "phase_done", phase: "maintenance" });
2352
2453
  await weeklyPhase(bc);
@@ -2368,7 +2469,7 @@ async function beat(deps) {
2368
2469
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "beat_agentless" });
2369
2470
  scheduleDeferredRetry(deps);
2370
2471
  }
2371
- writeBeatStatus(deps, { beatStart, wandered });
2472
+ writeBeatStatus(deps, { beatStart, wandered, away: pausedAway });
2372
2473
  } catch (e) {
2373
2474
  deps.ctx.logger.error("heartbeat: beat failed: %s", String(e).slice(0, 200));
2374
2475
  markHomeRotateIfOverflow(e);
@@ -2431,6 +2532,9 @@ function startOrchestrator(deps) {
2431
2532
  lastBeat = null;
2432
2533
  droppedStreak = 0;
2433
2534
  deferredRetries = 0;
2535
+ clearTokenSaverWake();
2536
+ pausedSince = 0;
2537
+ heldChecks = 0;
2434
2538
  if (retryTimer) {
2435
2539
  clearTimeout(retryTimer);
2436
2540
  retryTimer = void 0;
@@ -2471,7 +2575,9 @@ export {
2471
2575
  pickSpokenLine,
2472
2576
  spokeTextSince,
2473
2577
  TOKEN_SAVER_IDLE_SECONDS,
2578
+ TOKEN_SAVER_WAKE_MS,
2579
+ userActiveInBoundSessions,
2474
2580
  beat,
2475
2581
  startOrchestrator
2476
2582
  };
2477
- //# sourceMappingURL=chunk-AJ5HLAL7.js.map
2583
+ //# sourceMappingURL=chunk-TRO4E6WG.js.map