@kanadego/dsh-heartbeat 1.9.1 → 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.
Files changed (43) hide show
  1. package/README.md +351 -374
  2. package/client.js +96 -20
  3. package/config/policy.json +4 -3
  4. package/cordis.patch.yml +18 -39
  5. package/dist/{chunk-BQCXYE7N.js → chunk-3MDBU6WY.js} +2 -2
  6. package/dist/{chunk-OYVNWB5G.js → chunk-CUOSICYJ.js} +2 -2
  7. package/dist/{chunk-KOOQOMQY.js → chunk-JDH2LDFS.js} +2 -2
  8. package/dist/chunk-K5Y6JP2B.js +399 -0
  9. package/dist/chunk-K5Y6JP2B.js.map +1 -0
  10. package/dist/{chunk-QOQ7EPZZ.js → chunk-KB5SMG3F.js} +3 -3
  11. package/dist/{chunk-HT57FOEF.js → chunk-TRO4E6WG.js} +164 -35
  12. package/dist/chunk-TRO4E6WG.js.map +1 -0
  13. package/dist/{chunk-WRJCINZ7.js → chunk-WLZSABBH.js} +20 -6
  14. package/dist/chunk-WLZSABBH.js.map +1 -0
  15. package/dist/{chunk-2NB4QILF.js → chunk-XFNCTOW7.js} +2 -2
  16. package/dist/{chunk-ZLV4NMFX.js → chunk-XYYEJK3W.js} +4 -1
  17. package/dist/chunk-XYYEJK3W.js.map +1 -0
  18. package/dist/cli/index.js +10 -10
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/{inbox-JLT5G7TL.js → inbox-P26DDMWC.js} +3 -3
  21. package/dist/index.d.ts +35 -0
  22. package/dist/index.js +137 -26
  23. package/dist/index.js.map +1 -1
  24. package/dist/{migrate-WDVFHUP7.js → migrate-D4VKAKYT.js} +3 -3
  25. package/dist/{orchestrator-2ZLU7RGE.js → orchestrator-ZPRGVDCB.js} +16 -10
  26. package/dist/{pool-B34NTH6D.js → pool-UTXQJJ5C.js} +3 -3
  27. package/dist/{snapshot-JQDJ346Z.js → snapshot-K5VHETTK.js} +4 -4
  28. package/package.json +83 -83
  29. package/dist/chunk-7TW6DD6Q.js +0 -208
  30. package/dist/chunk-7TW6DD6Q.js.map +0 -1
  31. package/dist/chunk-HT57FOEF.js.map +0 -1
  32. package/dist/chunk-WRJCINZ7.js.map +0 -1
  33. package/dist/chunk-ZLV4NMFX.js.map +0 -1
  34. /package/dist/{chunk-BQCXYE7N.js.map → chunk-3MDBU6WY.js.map} +0 -0
  35. /package/dist/{chunk-OYVNWB5G.js.map → chunk-CUOSICYJ.js.map} +0 -0
  36. /package/dist/{chunk-KOOQOMQY.js.map → chunk-JDH2LDFS.js.map} +0 -0
  37. /package/dist/{chunk-QOQ7EPZZ.js.map → chunk-KB5SMG3F.js.map} +0 -0
  38. /package/dist/{chunk-2NB4QILF.js.map → chunk-XFNCTOW7.js.map} +0 -0
  39. /package/dist/{inbox-JLT5G7TL.js.map → inbox-P26DDMWC.js.map} +0 -0
  40. /package/dist/{migrate-WDVFHUP7.js.map → migrate-D4VKAKYT.js.map} +0 -0
  41. /package/dist/{orchestrator-2ZLU7RGE.js.map → orchestrator-ZPRGVDCB.js.map} +0 -0
  42. /package/dist/{pool-B34NTH6D.js.map → pool-UTXQJJ5C.js.map} +0 -0
  43. /package/dist/{snapshot-JQDJ346Z.js.map → snapshot-K5VHETTK.js.map} +0 -0
@@ -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-WRJCINZ7.js";
24
+ } from "./chunk-WLZSABBH.js";
24
25
  import {
25
26
  getRuntime
26
27
  } from "./chunk-3QJJRXIR.js";
@@ -31,17 +32,17 @@ import {
31
32
  loadPool,
32
33
  seedsFilePath,
33
34
  surfaceSeed
34
- } from "./chunk-KOOQOMQY.js";
35
+ } from "./chunk-JDH2LDFS.js";
35
36
  import {
36
37
  dedupeItems,
37
38
  inboxClear,
38
39
  inboxCount,
39
40
  inboxDrain,
40
41
  inboxFilePath
41
- } from "./chunk-BQCXYE7N.js";
42
+ } from "./chunk-3MDBU6WY.js";
42
43
  import {
43
44
  snapshotIfDue
44
- } from "./chunk-QOQ7EPZZ.js";
45
+ } from "./chunk-KB5SMG3F.js";
45
46
  import {
46
47
  appendAuditLine,
47
48
  applyOpsToDoc,
@@ -53,7 +54,7 @@ import {
53
54
  readAuditLines,
54
55
  readJournalTexts,
55
56
  runDeterministicAging
56
- } from "./chunk-OYVNWB5G.js";
57
+ } from "./chunk-CUOSICYJ.js";
57
58
  import {
58
59
  decryptFile,
59
60
  encryptFile,
@@ -63,7 +64,7 @@ import {
63
64
  saveEncryptedText,
64
65
  saveJson,
65
66
  writeText
66
- } from "./chunk-7TW6DD6Q.js";
67
+ } from "./chunk-K5Y6JP2B.js";
67
68
  import {
68
69
  atomicWriteJsonSync,
69
70
  burnFileSync
@@ -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));
@@ -990,7 +993,7 @@ async function runConsolidation(guard, paths, policy, llm, now = Date.now()) {
990
993
  const report = applyOpsToDoc(guard, paths.dataDir, doc, profileOpsCast, schema, policy, now);
991
994
  const aged = runDeterministicAging(doc, policy, now);
992
995
  persistWithJournal(guard, paths.dataDir, doc, { runId, applied: report.applied, rejected: report.rejected });
993
- const { addSeed: addSeed2, seedsFilePath: seedsFilePath2 } = await import("./pool-B34NTH6D.js");
996
+ const { addSeed: addSeed2, seedsFilePath: seedsFilePath2 } = await import("./pool-UTXQJJ5C.js");
994
997
  const seedAudit = (entry) => {
995
998
  try {
996
999
  appendAuditLine(paths.dataDir + "/logs/heartbeat.jsonl", entry);
@@ -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
 
@@ -1277,6 +1301,13 @@ function applyHeartbeatInterval(deps, intervalMin) {
1277
1301
  reschedule?.(v);
1278
1302
  appendAuditLine(deps.paths.logsDir + "/heartbeat.jsonl", { event: "interval_changed", intervalMin: v });
1279
1303
  }
1304
+ function looksPermanentResumeFailure(error) {
1305
+ return [
1306
+ /SessionFormatUnsupportedError/i,
1307
+ /refuses the transformed artifact/i,
1308
+ /session migration[^]*refus/i
1309
+ ].some((re) => re.test(error));
1310
+ }
1280
1311
  function sessionEvents(session, fromSeq) {
1281
1312
  if (!session) return [];
1282
1313
  if (typeof session.snapshotEvents === "function") {
@@ -1398,6 +1429,11 @@ function makeHomeSetup(deps) {
1398
1429
  return async (agentCtx) => {
1399
1430
  const notes = [];
1400
1431
  const presetId = deps.agentPreset ?? "heartbeat";
1432
+ try {
1433
+ await withTimeout(deps.presetRegistration?.() ?? Promise.resolve(), 3e4, "preset registration timeout (30s)");
1434
+ } catch (e) {
1435
+ notes.push(`preset-register=threw(${String(e).slice(0, 120)})`);
1436
+ }
1401
1437
  try {
1402
1438
  const presets = agentCtx.get("agentPresets");
1403
1439
  if (typeof presets?.mount !== "function") {
@@ -1539,12 +1575,15 @@ async function ensureAgent(deps) {
1539
1575
  agent = unwrap(handle);
1540
1576
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "agent_resume_ok", sessionId: savedId, model: agent.options?.model ?? "(none)" });
1541
1577
  } catch (resumeErr) {
1578
+ const resumeMessage = String(resumeErr);
1542
1579
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
1543
1580
  event: "agent_resume_failed",
1544
1581
  sessionId: savedId,
1545
- error: String(resumeErr).slice(0, 160)
1582
+ error: resumeMessage.slice(0, 160)
1546
1583
  });
1547
- if (!/not found/i.test(String(resumeErr))) {
1584
+ const gone = /not found/i.test(resumeMessage);
1585
+ const permanent = !gone && looksPermanentResumeFailure(resumeMessage);
1586
+ if (!gone && !permanent) {
1548
1587
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
1549
1588
  event: "agent_deferred",
1550
1589
  sessionId: savedId,
@@ -1553,6 +1592,13 @@ async function ensureAgent(deps) {
1553
1592
  agentPromise = null;
1554
1593
  return null;
1555
1594
  }
1595
+ if (permanent) {
1596
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
1597
+ event: "agent_resume_permanent",
1598
+ sessionId: savedId,
1599
+ error: resumeMessage.slice(0, 160)
1600
+ });
1601
+ }
1556
1602
  try {
1557
1603
  const handle = await withTimeout(Promise.resolve(ctx.agents.create({ sessionId: savedId, meta: { cwd: paths.dataDir }, ...agentOptions ? { agentOptions } : {}, setup })), 3e4, "agents.create (self-heal) timeout");
1558
1604
  agent = unwrap(handle);
@@ -1760,7 +1806,7 @@ async function observeBoundSessions(bc) {
1760
1806
  const { deps, now } = bc;
1761
1807
  const { guard, paths } = deps;
1762
1808
  const { loadBindings, observeTargets } = await import("./bindings-225SI7KC.js");
1763
- const { inboxAppend, inboxFilePath: inboxFilePath2 } = await import("./inbox-JLT5G7TL.js");
1809
+ const { inboxAppend, inboxFilePath: inboxFilePath2 } = await import("./inbox-P26DDMWC.js");
1764
1810
  const data = loadBindings(guard, paths.settingsDir);
1765
1811
  const targets = observeTargets(data);
1766
1812
  if (targets.length === 0) return;
@@ -1847,6 +1893,10 @@ async function weeklyPhase(bc) {
1847
1893
  text
1848
1894
  });
1849
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
+ }
1850
1900
  if (!inQuietHours(policy, now)) {
1851
1901
  try {
1852
1902
  await sendWeeklyReadyHint(paths);
@@ -2240,13 +2290,13 @@ function writeBeatStatus(deps, info) {
2240
2290
  const pulse = readPulse(guard, paths);
2241
2291
  const last = getLastBeat();
2242
2292
  const spokeThisBeat = last?.verdict === "spoke" && !!last.at && last.at >= info.beatStart;
2243
- const scene = deriveScene({
2293
+ const scene = info.away ? "away" : deriveScene({
2244
2294
  quietHours: inQuietHours(policy, Date.now()),
2245
2295
  spokeThisBeat,
2246
2296
  wanderedThisBeat: info.wandered,
2247
2297
  presence: pulse?.presence ?? "unknown"
2248
2298
  });
2249
- 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;
2250
2300
  writeStatus(guard, paths, {
2251
2301
  at: (/* @__PURE__ */ new Date()).toISOString(),
2252
2302
  scene,
@@ -2269,12 +2319,72 @@ function scheduleDeferredRetry(deps) {
2269
2319
  }, delayMs);
2270
2320
  }
2271
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
+ }
2272
2358
  async function tokenSaverActive(deps) {
2273
2359
  if (!getRuntime().flags.tokenSaver()) return false;
2360
+ if (await userActiveInBoundSessions(deps)) return false;
2274
2361
  if (await probeWorkstationLocked()) return true;
2275
2362
  const idle = await probeIdleSeconds(deps.guard, deps.paths);
2276
2363
  return idle >= TOKEN_SAVER_IDLE_SECONDS;
2277
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
+ }
2278
2388
  async function beat(deps) {
2279
2389
  if (beating) return;
2280
2390
  beating = true;
@@ -2318,13 +2428,26 @@ async function beat(deps) {
2318
2428
  return;
2319
2429
  }
2320
2430
  let wandered = false;
2431
+ let pausedAway = false;
2321
2432
  if (agent) {
2322
2433
  deferredRetries = 0;
2323
2434
  const bc = { deps, agent, now };
2324
2435
  if (await tokenSaverActive(deps)) {
2325
2436
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "silent", reason: "token-saver" });
2326
2437
  noteBeat("silent", { reason: "token-saver\uFF08\u4F60\u4E0D\u5728\uFF0C\u5FC3\u8DF3\u6302\u8D77\uFF09" });
2438
+ pausedAway = true;
2439
+ scheduleTokenSaverWake(deps);
2327
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;
2328
2451
  await withTimeout(maintenancePhase(bc), MAINTENANCE_TIMEOUT_MS, "maintenance");
2329
2452
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "phase_done", phase: "maintenance" });
2330
2453
  await weeklyPhase(bc);
@@ -2346,7 +2469,7 @@ async function beat(deps) {
2346
2469
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "beat_agentless" });
2347
2470
  scheduleDeferredRetry(deps);
2348
2471
  }
2349
- writeBeatStatus(deps, { beatStart, wandered });
2472
+ writeBeatStatus(deps, { beatStart, wandered, away: pausedAway });
2350
2473
  } catch (e) {
2351
2474
  deps.ctx.logger.error("heartbeat: beat failed: %s", String(e).slice(0, 200));
2352
2475
  markHomeRotateIfOverflow(e);
@@ -2409,6 +2532,9 @@ function startOrchestrator(deps) {
2409
2532
  lastBeat = null;
2410
2533
  droppedStreak = 0;
2411
2534
  deferredRetries = 0;
2535
+ clearTokenSaverWake();
2536
+ pausedSince = 0;
2537
+ heldChecks = 0;
2412
2538
  if (retryTimer) {
2413
2539
  clearTimeout(retryTimer);
2414
2540
  retryTimer = void 0;
@@ -2428,6 +2554,7 @@ export {
2428
2554
  StatusReader,
2429
2555
  buildDigest,
2430
2556
  applyHeartbeatInterval,
2557
+ looksPermanentResumeFailure,
2431
2558
  sessionEvents,
2432
2559
  sessionEventCount,
2433
2560
  HOME_ROTATE_EVENT_COUNT,
@@ -2448,7 +2575,9 @@ export {
2448
2575
  pickSpokenLine,
2449
2576
  spokeTextSince,
2450
2577
  TOKEN_SAVER_IDLE_SECONDS,
2578
+ TOKEN_SAVER_WAKE_MS,
2579
+ userActiveInBoundSessions,
2451
2580
  beat,
2452
2581
  startOrchestrator
2453
2582
  };
2454
- //# sourceMappingURL=chunk-HT57FOEF.js.map
2583
+ //# sourceMappingURL=chunk-TRO4E6WG.js.map