@kanadego/dsh-heartbeat 1.7.4 → 1.9.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/dist/index.js CHANGED
@@ -1,29 +1,33 @@
1
1
  import {
2
2
  adviseRefillWander,
3
3
  adviseWander,
4
- appendAuditLine,
5
- applyOpsToDoc,
6
- atomicWriteJsonSync,
4
+ appendEntry,
5
+ buildWeeklyPrompt,
7
6
  completeWander,
8
7
  createPathGuard,
9
8
  deepMerge,
10
9
  describeInstall,
11
10
  ensureRegistered,
11
+ ensureWeeklyAnchor,
12
12
  installBundledPreset,
13
13
  ledgerFilePath,
14
+ listWeeklyReports,
14
15
  loadPolicy,
15
- loadProfile,
16
- loadProfileSchema,
16
+ markDone,
17
17
  pendingOlderThan,
18
- persistWithJournal,
19
- profileFilePath,
20
- pruneAuditFile,
21
- runDeterministicAging,
18
+ preferenceFilePath,
19
+ readLedger,
20
+ readWeeklyReport,
21
+ recordDelivery,
22
+ renderTemplateReport,
23
+ saveWeeklyReport,
22
24
  scanPending,
23
25
  sendNewMessageHint,
26
+ sendWeeklyReadyHint,
24
27
  updateUserPolicy,
25
- userPresetRoot
26
- } from "./chunk-ON4MSU6E.js";
28
+ userPresetRoot,
29
+ weeklyDue
30
+ } from "./chunk-2SYXKKZF.js";
27
31
  import {
28
32
  getRuntime,
29
33
  setRuntime
@@ -39,14 +43,35 @@ import {
39
43
  restoreSeed,
40
44
  seedsFilePath,
41
45
  surfaceSeed
42
- } from "./chunk-SVP2NDRF.js";
46
+ } from "./chunk-PQTSXW4F.js";
43
47
  import {
44
48
  dedupeItems,
45
49
  inboxClear,
46
50
  inboxCount,
47
51
  inboxDrain,
48
52
  inboxFilePath
49
- } from "./chunk-4UE74TUB.js";
53
+ } from "./chunk-63FUJLIF.js";
54
+ import {
55
+ snapshotIfDue
56
+ } from "./chunk-RB3NLLGF.js";
57
+ import {
58
+ appendAuditLine,
59
+ applyOpsToDoc,
60
+ atomicWriteJsonSync,
61
+ journalFilePath,
62
+ loadProfile,
63
+ loadProfileSchema,
64
+ persistWithJournal,
65
+ profileFilePath,
66
+ pruneAuditFile,
67
+ readAuditLines,
68
+ runDeterministicAging
69
+ } from "./chunk-VIZNIQLK.js";
70
+ import {
71
+ addBinding,
72
+ loadBindings,
73
+ removeBinding
74
+ } from "./chunk-J6ZTRFFW.js";
50
75
  import {
51
76
  decryptFile,
52
77
  encryptFile,
@@ -57,14 +82,9 @@ import {
57
82
  saveEncryptedText,
58
83
  saveJson,
59
84
  writeText
60
- } from "./chunk-LLD7LUNN.js";
61
- import {
62
- addBinding,
63
- loadBindings,
64
- removeBinding
65
- } from "./chunk-J6ZTRFFW.js";
85
+ } from "./chunk-IFTFDHZX.js";
66
86
 
67
- // node_modules/@deepseek-ai/cosmokit/lib/index.js
87
+ // node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.3/node_modules/@deepseek-ai/cosmokit/lib/index.js
68
88
  function isNullable(value) {
69
89
  return value === null || value === void 0;
70
90
  }
@@ -83,32 +103,6 @@ function pick(source, keys, forced) {
83
103
  for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
84
104
  return result;
85
105
  }
86
- var write = /* @__PURE__ */ Symbol.for("cosmokit.volatile.write");
87
- function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
88
- if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
89
- if (value === null || typeof value !== "object") return value;
90
- if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
91
- ancestors.add(value);
92
- try {
93
- if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
94
- if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
95
- return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
96
- } finally {
97
- ancestors.delete(value);
98
- }
99
- }
100
- function createVolatile(value) {
101
- let current = snapshot(value);
102
- return Object.freeze({
103
- get: () => current,
104
- [write]: (value2) => {
105
- current = value2;
106
- }
107
- });
108
- }
109
- function isVolatile(value) {
110
- return typeof value === "object" && value !== null && write in value;
111
- }
112
106
  function is(type, value) {
113
107
  if (arguments.length === 1) return (value2) => is(type, value2);
114
108
  return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
@@ -187,37 +181,24 @@ function clone(source, refs = /* @__PURE__ */ new Map()) {
187
181
  return result;
188
182
  }
189
183
  function deepEqual(a, b, strict) {
190
- const ancestors = /* @__PURE__ */ new Set();
191
- function compare(a2, b2) {
192
- if (a2 === b2) return true;
193
- if (isVolatile(a2) || isVolatile(b2)) return isVolatile(a2) && isVolatile(b2);
194
- if (!strict && isNullable(a2) && isNullable(b2)) return true;
195
- if (typeof a2 !== typeof b2 || typeof a2 !== "object" || !a2 || !b2) return false;
196
- if (ancestors.has(a2)) return false;
197
- function check(test, then) {
198
- return test(a2) ? test(b2) ? then(a2, b2) : false : test(b2) ? false : void 0;
199
- }
200
- ancestors.add(a2);
201
- try {
202
- return check(Array.isArray, (a3, b3) => {
203
- if (a3.length !== b3.length) return false;
204
- for (let index = 0; index < a3.length; index++) if (!compare(a3[index], b3[index])) return false;
205
- return true;
206
- }) ?? check(is("Date"), (a3, b3) => a3.valueOf() === b3.valueOf()) ?? check(is("URL"), (a3, b3) => a3.href === b3.href) ?? check(is("RegExp"), (a3, b3) => a3.source === b3.source && a3.flags === b3.flags) ?? check(isArrayBufferLike, (a3, b3) => {
207
- if (a3.byteLength !== b3.byteLength) return false;
208
- const viewA = new Uint8Array(a3);
209
- const viewB = new Uint8Array(b3);
210
- for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
211
- return true;
212
- }) ?? ((!strict || [a2, b2].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
213
- ...a2,
214
- ...b2
215
- }).every((key) => compare(a2[key], b2[key])));
216
- } finally {
217
- ancestors.delete(a2);
218
- }
219
- }
220
- return compare(a, b);
184
+ if (a === b) return true;
185
+ if (!strict && isNullable(a) && isNullable(b)) return true;
186
+ if (typeof a !== typeof b) return false;
187
+ if (typeof a !== "object") return false;
188
+ if (!a || !b) return false;
189
+ function check(test, then) {
190
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
191
+ }
192
+ return check(Array.isArray, (a2, b2) => a2.length === b2.length && a2.every((item, index) => deepEqual(item, b2[index]))) ?? check(is("Date"), (a2, b2) => a2.valueOf() === b2.valueOf()) ?? check(is("RegExp"), (a2, b2) => a2.source === b2.source && a2.flags === b2.flags) ?? check(isArrayBufferLike, (a2, b2) => {
193
+ if (a2.byteLength !== b2.byteLength) return false;
194
+ const viewA = new Uint8Array(a2);
195
+ const viewB = new Uint8Array(b2);
196
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
197
+ return true;
198
+ }) ?? Object.keys({
199
+ ...a,
200
+ ...b
201
+ }).every((key) => deepEqual(a[key], b[key], strict));
221
202
  }
222
203
  var Time;
223
204
  (function(Time2) {
@@ -289,7 +270,7 @@ var Time;
289
270
  Time2.template = template;
290
271
  })(Time || (Time = {}));
291
272
 
292
- // node_modules/@deepseek-ai/schemastery/lib/index.mjs
273
+ // node_modules/.pnpm/@deepseek-ai+schemastery@3.18.2/node_modules/@deepseek-ai/schemastery/lib/index.mjs
293
274
  var kSchema = /* @__PURE__ */ Symbol.for("schemastery");
294
275
  var kValidationError = /* @__PURE__ */ Symbol.for("ValidationError");
295
276
  globalThis.__schemastery_index__ ??= 0;
@@ -466,7 +447,6 @@ Schema.prototype.pattern = function pattern(regexp) {
466
447
  return schema;
467
448
  };
468
449
  Schema.prototype.simplify = function simplify(value) {
469
- if (isVolatile(value)) value = value.get();
470
450
  if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
471
451
  if (isNullable(value)) return value;
472
452
  if (this.type === "object" || this.type === "dict") {
@@ -524,49 +504,12 @@ for (const key of [
524
504
  };
525
505
  return schema;
526
506
  } });
527
- Schema.prototype.volatile = function volatile() {
528
- if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
529
- return this.extra("volatile", true);
530
- };
531
507
  var resolvers = {};
532
- var checkedVolatile = /* @__PURE__ */ Symbol("checked-volatile-schema");
533
- function validateVolatileSchema(schema, path13 = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
534
- const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
535
- if (states.has(blocked)) return;
536
- states.add(blocked);
537
- seen.set(schema, states);
538
- if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path: path13 });
539
- const nested = blocked || !!schema.meta?.volatile;
540
- if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path13, key], nested, seen);
541
- if (schema.sKey) validateVolatileSchema(schema.sKey, [...path13, "<key>"], true, seen);
542
- if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path13, "*"], true, seen);
543
- if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path13, String(index)], true, seen);
544
- }
545
508
  Schema.extend = function extend(type, resolve2) {
546
509
  resolvers[type] = resolve2;
547
510
  };
548
511
  Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
549
512
  if (!schema) return [data];
550
- if (!options[checkedVolatile]) {
551
- validateVolatileSchema(schema, options.path);
552
- options = {
553
- ...options,
554
- [checkedVolatile]: true
555
- };
556
- }
557
- if (schema.meta?.volatile) {
558
- const inner = Schema(schema);
559
- inner.meta = {
560
- ...schema.meta,
561
- volatile: false
562
- };
563
- const [value, adapted] = Schema.resolve(data, inner, options, strict);
564
- try {
565
- return [createVolatile(value), adapted];
566
- } catch (error) {
567
- throw new ValidationError(error instanceof Error ? error.message : String(error), options);
568
- }
569
- }
570
513
  if (options.ignore?.(data, schema)) return [data];
571
514
  if (isNullable(data) && schema.type !== "lazy") {
572
515
  if (schema.meta.required) throw new ValidationError(`missing required value`, options);
@@ -674,7 +617,6 @@ Schema.extend("lazy", (data, schema, options, strict) => {
674
617
  ...schema.meta,
675
618
  ...schema.inner.meta
676
619
  };
677
- validateVolatileSchema(schema.inner, options.path, true);
678
620
  }
679
621
  return Schema.resolve(data, schema.inner, options, strict);
680
622
  });
@@ -774,7 +716,7 @@ function property(data, key, schema, options) {
774
716
  } catch (e) {
775
717
  if (!options?.autofix) throw e;
776
718
  delete data[key];
777
- return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
719
+ return schema.meta.default;
778
720
  }
779
721
  }
780
722
  Schema.extend("array", (data, { inner, meta }, options) => {
@@ -975,8 +917,8 @@ function saveUiConfig(guard, settingsDir, patch) {
975
917
 
976
918
  // src/core/orchestrator.ts
977
919
  import { randomUUID as randomUUID2 } from "crypto";
978
- import fs9 from "fs";
979
- import path9 from "path";
920
+ import fs10 from "fs";
921
+ import path10 from "path";
980
922
 
981
923
  // src/env/envpulse.ts
982
924
  import fs3 from "fs";
@@ -1104,7 +1046,7 @@ function collectPulse(guard, paths, rules, fgProcess = null, now = /* @__PURE__
1104
1046
  const idle = probeIdle(guard, paths);
1105
1047
  const windowClass = classifyProcess(fgProcess, rules);
1106
1048
  const t = timeContext(now);
1107
- const snapshot2 = {
1049
+ const snapshot = {
1108
1050
  takenAt: now.toISOString(),
1109
1051
  idleSeconds: idle,
1110
1052
  presence: presenceOf(idle, windowClass),
@@ -1115,24 +1057,24 @@ function collectPulse(guard, paths, rules, fgProcess = null, now = /* @__PURE__
1115
1057
  festival: t.festival
1116
1058
  };
1117
1059
  try {
1118
- fs3.writeFileSync(path3.join(paths.dataDir, "envpulse.json"), JSON.stringify(snapshot2, null, 1), "utf8");
1060
+ fs3.writeFileSync(path3.join(paths.dataDir, "envpulse.json"), JSON.stringify(snapshot, null, 1), "utf8");
1119
1061
  } catch {
1120
1062
  }
1121
- writePulseStream(paths, snapshot2);
1122
- return snapshot2;
1063
+ writePulseStream(paths, snapshot);
1064
+ return snapshot;
1123
1065
  }
1124
- function writePulseStream(paths, snapshot2) {
1066
+ function writePulseStream(paths, snapshot) {
1125
1067
  try {
1126
1068
  appendAuditLine(path3.join(paths.logsDir, "envpulse.jsonl"), {
1127
1069
  event: "pulse",
1128
- takenAt: snapshot2.takenAt,
1129
- idleSeconds: snapshot2.idleSeconds,
1130
- presence: snapshot2.presence,
1131
- windowClass: snapshot2.windowClass,
1132
- daypart: snapshot2.daypart,
1133
- weekday: snapshot2.weekday,
1134
- isWeekend: snapshot2.isWeekend,
1135
- festival: snapshot2.festival
1070
+ takenAt: snapshot.takenAt,
1071
+ idleSeconds: snapshot.idleSeconds,
1072
+ presence: snapshot.presence,
1073
+ windowClass: snapshot.windowClass,
1074
+ daypart: snapshot.daypart,
1075
+ weekday: snapshot.weekday,
1076
+ isWeekend: snapshot.isWeekend,
1077
+ festival: snapshot.festival
1136
1078
  });
1137
1079
  } catch {
1138
1080
  }
@@ -1179,10 +1121,10 @@ function collectScreen(guard, paths, now = Date.now()) {
1179
1121
  } catch {
1180
1122
  }
1181
1123
  }
1182
- const { shot_path: _drop, ...clean } = meta;
1124
+ const { shot_path: _drop, ...clean2 } = meta;
1183
1125
  void _drop;
1184
1126
  encJson = path4.join(paths.tmpDir, `screen.enc.${now}.json`);
1185
- fs4.writeFileSync(encJson, JSON.stringify(clean, null, 1), "utf8");
1127
+ fs4.writeFileSync(encJson, JSON.stringify(clean2, null, 1), "utf8");
1186
1128
  encryptFile(guard, encJson, screenJsonPath(paths));
1187
1129
  return {
1188
1130
  ok: true,
@@ -1473,7 +1415,7 @@ function assembleCandidates(seeds, opts = {}) {
1473
1415
  }
1474
1416
  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";
1475
1417
  function materialLines(materials) {
1476
- return materials.map((m) => m.text.trim());
1418
+ return materials.map((m) => `- [${m.id}] ${m.text.trim()}`);
1477
1419
  }
1478
1420
  function wantHonestOption(materials, threshold = 2) {
1479
1421
  return materials.filter((m) => m.used >= 1).length >= threshold;
@@ -1485,22 +1427,60 @@ function buildMaterialPrompt(materials, opts = {}) {
1485
1427
  }
1486
1428
  lines.push(PACKAGE_DECLARE);
1487
1429
  const items = materialLines(materials);
1488
- for (const it of items) lines.push("- " + it);
1430
+ for (const it of items) lines.push(it);
1489
1431
  if (wantHonestOption(materials, opts.threshold ?? 2)) {
1490
1432
  lines.push("(\u6216\u8005\u4E5F\u53EF\u4EE5\u8BF4\u4E00\u53E5\u771F\u5FC3\u8BDD,\u4E0D\u5E26\u7D20\u6750)");
1491
1433
  }
1434
+ if (opts.deliveryId) {
1435
+ lines.push(`(\u672C\u6B21\u6295\u9012\u7F16\u53F7:${opts.deliveryId},\u62A5\u8D26\u65F6\u539F\u6837\u586B\u8FDB delivery_id)`);
1436
+ }
1437
+ lines.push(REPORT_DECLARE);
1492
1438
  return lines.join("\n");
1493
1439
  }
1494
- function attributionIds(candidates, output, explicitIds = []) {
1495
- const used = new Set(explicitIds);
1496
- const b = output.trim();
1497
- for (const c of candidates) {
1498
- const a = c.text.trim();
1499
- if (a.length >= 8 && (b.includes(a.slice(0, Math.min(20, a.length))) || a.includes(b.slice(0, Math.min(20, b.length))))) {
1500
- used.add(c.id);
1501
- }
1440
+ 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";
1441
+ function explicitIds(candidates, ids = []) {
1442
+ const known = new Set(candidates.map((c) => c.id));
1443
+ return [...new Set(ids)].filter((id) => known.has(id));
1444
+ }
1445
+ function pickReport(reports, deliveryId) {
1446
+ const exact = reports.filter((r) => r.deliveryId && r.deliveryId === deliveryId);
1447
+ if (exact.length > 0) return exact[exact.length - 1];
1448
+ const anonymous = reports.filter((r) => !r.deliveryId);
1449
+ return anonymous.length > 0 ? anonymous[anonymous.length - 1] : null;
1450
+ }
1451
+ function reconcileDelivery(materials, report, decisionIds, spokeText) {
1452
+ if (report && report.spoken === "silent" && !spokeText) {
1453
+ return {
1454
+ spoken: "silent",
1455
+ seedIds: [],
1456
+ profileIds: explicitIds(materials, report.profileIds),
1457
+ ...report.reason ? { reason: report.reason } : {},
1458
+ source: "report"
1459
+ };
1460
+ }
1461
+ if (!spokeText) {
1462
+ return { spoken: "silent", seedIds: [], profileIds: [], source: "none" };
1502
1463
  }
1503
- return [...used];
1464
+ if (report) {
1465
+ const seedIds = explicitIds(materials, report.seedIds);
1466
+ return {
1467
+ // 报账说 silent 但实际出了声:话已成事实,降级记 heartfelt(说了但不带素材)。
1468
+ // v1.9.0: 报账说 material 但 id 一个都不在本包里(抄错/上一轮残留)同样降级——
1469
+ // 绝不凭一句「我用了素材」记名。
1470
+ spoken: report.spoken === "silent" || seedIds.length === 0 ? "heartfelt" : report.spoken,
1471
+ seedIds,
1472
+ profileIds: explicitIds(materials, report.profileIds),
1473
+ ...report.reason ? { reason: report.reason } : {},
1474
+ source: "report"
1475
+ };
1476
+ }
1477
+ const ids = explicitIds(materials, decisionIds);
1478
+ return {
1479
+ spoken: ids.length > 0 ? "material" : "heartfelt",
1480
+ seedIds: ids,
1481
+ profileIds: [],
1482
+ source: ids.length > 0 ? "decision" : "none"
1483
+ };
1504
1484
  }
1505
1485
  function buildRuminationPrompt(input) {
1506
1486
  const max = input.max ?? 3;
@@ -1541,9 +1521,128 @@ function buildRuminationPrompt(input) {
1541
1521
  return lines.join("\n");
1542
1522
  }
1543
1523
 
1524
+ // src/seeds/report.ts
1525
+ import path7 from "path";
1526
+ var REPORT_REASONS = ["\u4E0D\u60F3\u8BF4\u8BDD", "\u7D20\u6750\u4E0D\u642D", "\u5728\u5FD9\u6216\u521A\u804A\u8FC7"];
1527
+ function reportFilePath(dataDir) {
1528
+ return path7.join(dataDir, "seed_report.jsonl");
1529
+ }
1530
+ var REPORT_KEEP = 400;
1531
+ function parseEntry(obj) {
1532
+ const e = obj;
1533
+ if (!e || typeof e.ts !== "number" || !Number.isFinite(e.ts)) return null;
1534
+ if (e.spoken !== "material" && e.spoken !== "heartfelt" && e.spoken !== "silent") return null;
1535
+ const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
1536
+ const ids = arr(e.seedIds);
1537
+ const pids = arr(e.profileIds);
1538
+ const reason = REPORT_REASONS.includes(e.reason) ? e.reason : void 0;
1539
+ const did = typeof e.deliveryId === "string" && e.deliveryId.trim() ? e.deliveryId.trim() : "";
1540
+ return {
1541
+ ts: e.ts,
1542
+ spoken: e.spoken,
1543
+ seedIds: ids,
1544
+ profileIds: pids,
1545
+ ...did ? { deliveryId: did } : {},
1546
+ ...reason ? { reason } : {}
1547
+ };
1548
+ }
1549
+ function appendSeedReport(guard, file, entry) {
1550
+ const raw = loadEncryptedText(guard, file) ?? "";
1551
+ const lines = raw.split("\n").filter((l) => l.trim());
1552
+ lines.push(JSON.stringify(entry));
1553
+ saveEncryptedText(guard, file, lines.slice(-REPORT_KEEP).join("\n") + "\n");
1554
+ }
1555
+ function readSeedReports(guard, file) {
1556
+ const raw = loadEncryptedText(guard, file);
1557
+ if (!raw) return [];
1558
+ const out = [];
1559
+ for (const line of raw.split("\n")) {
1560
+ const t = line.trim();
1561
+ if (!t) continue;
1562
+ try {
1563
+ const e = parseEntry(JSON.parse(t));
1564
+ if (e) out.push(e);
1565
+ } catch {
1566
+ }
1567
+ }
1568
+ return out;
1569
+ }
1570
+ function readSeedReportsSince(guard, file, sinceTs) {
1571
+ return readSeedReports(guard, file).filter((e) => e.ts >= sinceTs);
1572
+ }
1573
+ function cleanIds(v) {
1574
+ return Array.isArray(v) ? v.map((x) => String(x ?? "").replace(/[[\]{}"'`]/g, "").trim().replace(/\s+/g, "")).filter((s) => s.length > 0 && s.length <= 24) : [];
1575
+ }
1576
+ function buildSeedReportTool(guard, file) {
1577
+ return {
1578
+ name: "seed_report",
1579
+ description: "\u7D20\u6750\u62A5\u8D26\u5DE5\u5177\uFF08\u5FC3\u8DF3\u63D2\u4EF6\uFF09\u3002\u6536\u5230\u5FC3\u8DF3\u7D20\u6750\u5305\u540E\u5FC5\u987B\u8C03\u7528\u4E00\u6B21\u672C\u5DE5\u5177\u62A5\u8D26\uFF0C\u8BF4\u4E0D\u8BF4\u3001\u7528\u6CA1\u7528\u90FD\u8981\u62A5\uFF1Aspoken=material \u8868\u793A\u8BF4\u4E86\u7D20\u6750\u91CC\u7684\u5185\u5BB9\uFF08\u6B64\u65F6 seed_ids \u5FC5\u586B\uFF0C\u586B\u5B9E\u9645\u7528\u5230\u7684\u7D20\u6750\u7F16\u53F7\uFF0C\u53EF\u591A\u6761\uFF09\uFF1Bspoken=heartfelt \u8868\u793A\u8BF4\u4E86\u4E0D\u5E26\u7D20\u6750\u7684\u8BDD\uFF08\u771F\u5FC3\u8BDD\u6216\u77ED\u5E94\u7B54\uFF09\uFF1Bspoken=silent \u8868\u793A\u6CA1\u8BF4\u8BDD\u3002\u628A\u5F53\u6210\u8BDD\u9898\u804A\u4E86\u7684\u753B\u50CF\u6761\u76EE\u7F16\u53F7\u586B\u8FDB profile_ids\u3002\u6CA1\u7528\u7D20\u6750\u65F6\u7528 reason \u8BB0\u4E00\u7B14\u539F\u56E0\uFF1A\u4E0D\u60F3\u8BF4\u8BDD / \u7D20\u6750\u4E0D\u642D / \u5728\u5FD9\u6216\u521A\u804A\u8FC7\u3002\u4E00\u6B21\u6295\u9012\u53EA\u62A5\u4E00\u6B21\u8D26\uFF0C\u5E76\u628A\u7D20\u6750\u5305\u91CC\u7684\u672C\u6B21\u6295\u9012\u7F16\u53F7\u539F\u6837\u586B\u8FDB delivery_id\u3002",
1580
+ parameters: {
1581
+ type: "object",
1582
+ properties: {
1583
+ spoken: { type: "string", enum: ["material", "heartfelt", "silent"], description: "material=\u8BF4\u4E86\u7D20\u6750\uFF0Cheartfelt=\u8BF4\u4E86\u4E0D\u5E26\u7D20\u6750\u7684\u8BDD\uFF0Csilent=\u6CA1\u8BF4\u8BDD" },
1584
+ seed_ids: { type: "array", items: { type: "string" }, description: 'spoken=material \u65F6\u5FC5\u586B\uFF1A\u5B9E\u9645\u7528\u5230\u7684\u7D20\u6750\u7F16\u53F7\uFF0C\u53EA\u586B\u7F16\u53F7\u672C\u8EAB\uFF08\u5982 ["s3"]\uFF09\uFF0C\u4E0D\u8981\u5E26\u65B9\u62EC\u53F7' },
1585
+ profile_ids: { type: "array", items: { type: "string" }, description: "\u5B9E\u9645\u5F53\u6210\u8BDD\u9898\u804A\u4E86\u7684\u753B\u50CF\u6761\u76EE\u7F16\u53F7\uFF08\u95F2\u7740\u6A21\u5F0F\u7D20\u6750\u5305\u91CC\u624D\u6709\uFF09" },
1586
+ reason: { type: "string", enum: [...REPORT_REASONS], description: "\u6CA1\u7528\u7D20\u6750\u65F6\u7684\u539F\u56E0\uFF0C\u9009\u586B" },
1587
+ delivery_id: { type: "string", description: "\u7D20\u6750\u5305\u91CC\u7684\u672C\u6B21\u6295\u9012\u7F16\u53F7\uFF08\u5F62\u5982 d1a2b3c4\uFF09\uFF0C\u539F\u6837\u56DE\u586B" }
1588
+ },
1589
+ required: ["spoken"],
1590
+ additionalProperties: false
1591
+ },
1592
+ output: {
1593
+ schema: {
1594
+ type: "object",
1595
+ properties: { ok: { type: "boolean" }, message: { type: "string" } },
1596
+ required: ["ok", "message"],
1597
+ additionalProperties: false
1598
+ },
1599
+ render: (_args, value) => {
1600
+ const v = value;
1601
+ return [{ type: "text", text: String(v.message ?? "") }];
1602
+ }
1603
+ },
1604
+ // Whole-file load+save through DPAPI, and the report is filed while the
1605
+ // persona turn is still finishing — 0.7–2.7 s measured here, so 5 s was
1606
+ // too tight for a slow disk (raised 2026-10-06).
1607
+ timeoutMs: 15e3,
1608
+ // whole-file rewrite on one encrypted file; never parallel
1609
+ isConcurrencySafe: () => false,
1610
+ async execute(args) {
1611
+ const a = args ?? {};
1612
+ const spoken = String(a.spoken ?? "");
1613
+ if (spoken !== "material" && spoken !== "heartfelt" && spoken !== "silent") {
1614
+ return { ok: false, message: "spoken \u5FC5\u987B\u662F material / heartfelt / silent \u4E4B\u4E00" };
1615
+ }
1616
+ const seedIds = cleanIds(a.seed_ids);
1617
+ const profileIds = cleanIds(a.profile_ids);
1618
+ if (spoken === "material" && seedIds.length === 0) {
1619
+ return { ok: false, message: "spoken=material \u9700\u8981 seed_ids\uFF1A\u628A\u5B9E\u9645\u7528\u5230\u7684\u7D20\u6750\u7F16\u53F7\u586B\u8FDB\u6765" };
1620
+ }
1621
+ const reasonRaw = String(a.reason ?? "");
1622
+ const reason = REPORT_REASONS.includes(reasonRaw) ? reasonRaw : void 0;
1623
+ const deliveryId = String(a.delivery_id ?? "").trim().slice(0, 64);
1624
+ const entry = {
1625
+ ts: Date.now(),
1626
+ spoken,
1627
+ seedIds,
1628
+ profileIds,
1629
+ ...deliveryId ? { deliveryId } : {}
1630
+ };
1631
+ if (spoken !== "material" && reason) entry.reason = reason;
1632
+ appendSeedReport(guard, file, entry);
1633
+ const bits = [`spoken=${entry.spoken}`];
1634
+ if (entry.deliveryId) bits.push(`delivery_id=${entry.deliveryId}`);
1635
+ if (entry.seedIds.length) bits.push(`seed_ids=${entry.seedIds.join(",")}`);
1636
+ if (entry.profileIds.length) bits.push(`profile_ids=${entry.profileIds.join(",")}`);
1637
+ if (entry.reason) bits.push(`reason=${entry.reason}`);
1638
+ return { ok: true, message: `\u5DF2\u62A5\u8D26\uFF1A${bits.join(" ")}` };
1639
+ }
1640
+ };
1641
+ }
1642
+
1544
1643
  // src/statusbar/store.ts
1545
1644
  import fs7 from "fs";
1546
- import path7 from "path";
1645
+ import path8 from "path";
1547
1646
  function deriveScene(input) {
1548
1647
  if (input.quietHours) return "quiet-hours";
1549
1648
  if (input.spokeThisBeat) return "just-spoke";
@@ -1558,7 +1657,7 @@ function clampNote(note) {
1558
1657
  return t.length <= 30 ? t : t.slice(0, 30);
1559
1658
  }
1560
1659
  function statusFilePath(dataDir) {
1561
- return path7.join(dataDir, "settings", "status.json");
1660
+ return path8.join(dataDir, "settings", "status.json");
1562
1661
  }
1563
1662
  function writeStatus(guard, paths, state) {
1564
1663
  atomicWriteJsonSync(guard.assert(statusFilePath(paths.dataDir)), state);
@@ -1733,7 +1832,13 @@ async function runConsolidation(guard, paths, policy, llm, now = Date.now()) {
1733
1832
  const report = applyOpsToDoc(guard, paths.dataDir, doc, profileOpsCast, schema, policy, now);
1734
1833
  const aged = runDeterministicAging(doc, policy, now);
1735
1834
  persistWithJournal(guard, paths.dataDir, doc, { runId, applied: report.applied, rejected: report.rejected });
1736
- const { addSeed: addSeed2, seedsFilePath: seedsFilePath2 } = await import("./pool-YAIBE2L5.js");
1835
+ const { addSeed: addSeed2, seedsFilePath: seedsFilePath2 } = await import("./pool-7I4VACG2.js");
1836
+ const seedAudit = (entry) => {
1837
+ try {
1838
+ appendAuditLine(paths.dataDir + "/logs/heartbeat.jsonl", entry);
1839
+ } catch {
1840
+ }
1841
+ };
1737
1842
  let chatSeedsAdded = 0;
1738
1843
  for (const cs of chatSeeds) {
1739
1844
  try {
@@ -1742,7 +1847,7 @@ async function runConsolidation(guard, paths, policy, llm, now = Date.now()) {
1742
1847
  ...cs.topic ? { topic: cs.topic } : {},
1743
1848
  source: "chat",
1744
1849
  tag: "scene"
1745
- }, now);
1850
+ }, now, seedAudit);
1746
1851
  chatSeedsAdded += 1;
1747
1852
  } catch {
1748
1853
  }
@@ -1776,6 +1881,10 @@ function fmtEntry(prefix, content, opts) {
1776
1881
  const flag = opts.low ? "\uFF08\u4E45\u672A\u9A8C\u8BC1\uFF09" : "";
1777
1882
  return `${prefix}${content}${flag} [conf ${opts.confidence.toFixed(2)}]`;
1778
1883
  }
1884
+ function profileTopicEntries(doc, topN) {
1885
+ const score = (e) => e.confidence * 0.7 + 1 / (1 + Math.max(0, Date.now() - Date.parse(e.updatedAt)) / 864e5) * 0.3;
1886
+ return [...doc.partitions.interest.entries, ...doc.partitions.projects.entries].filter((e) => e.validTo === null).sort((a, b) => score(b) - score(a)).slice(0, topN);
1887
+ }
1779
1888
  function buildDigest(guard, paths, policy, input = {}) {
1780
1889
  const doc = loadProfile(guard, profileFilePath(paths.dataDir));
1781
1890
  const topN = input.topN ?? 8;
@@ -1793,8 +1902,7 @@ function buildDigest(guard, paths, policy, input = {}) {
1793
1902
  `[\u65F6\u95F4\u611F] ${rhythmLine}${input.windowClass ? ` | \u5F53\u524D\u7A97\u53E3\u7C7B\u522B: ${input.windowClass}` : ""}`,
1794
1903
  ...comm
1795
1904
  ];
1796
- const score = (e) => e.confidence * 0.7 + 1 / (1 + Math.max(0, Date.now() - Date.parse(e.updatedAt)) / 864e5) * 0.3;
1797
- const topicEntries = [...doc.partitions.interest.entries, ...doc.partitions.projects.entries].filter((e) => e.validTo === null).sort((a, b) => score(b) - score(a)).slice(0, topN).map((e) => {
1905
+ const topicEntries = profileTopicEntries(doc, topN).map((e) => {
1798
1906
  const prefix = e.partition === "projects" ? "- \u8FDB\u884C\u4E2D: " : "- \u5174\u8DA3: ";
1799
1907
  return fmtEntry(prefix, `${e.topic}/${e.subTopic}: ${e.content}`, { low: e.lowActivity, confidence: e.confidence });
1800
1908
  });
@@ -1822,11 +1930,11 @@ function buildDigest(guard, paths, policy, input = {}) {
1822
1930
 
1823
1931
  // src/rhythm/rhythm.ts
1824
1932
  import fs8 from "fs";
1825
- import path8 from "path";
1933
+ import path9 from "path";
1826
1934
  var DAY_MS = 864e5;
1827
1935
  var TAU_DAYS = 10;
1828
1936
  function rhythmFilePath(paths) {
1829
- return path8.join(paths.dataDir, "profile_rhythm.json");
1937
+ return path9.join(paths.dataDir, "profile_rhythm.json");
1830
1938
  }
1831
1939
  function loadRhythm(paths) {
1832
1940
  try {
@@ -1863,6 +1971,142 @@ function recordPresence(paths, env, now = Date.now()) {
1863
1971
  if (state.days.length > 30) state.days.shift();
1864
1972
  saveRhythm(paths, state);
1865
1973
  }
1974
+ function summarizeRhythm(paths) {
1975
+ const state = loadRhythm(paths);
1976
+ const score = [];
1977
+ for (const wd of Object.keys(state.histogram)) {
1978
+ for (const h of Object.keys(state.histogram[wd])) {
1979
+ const cell = state.histogram[wd][h];
1980
+ score.push({ key: `${h}\u65F6(\u5468${"\u65E5\u4E00\u4E8C\u4E09\u56DB\u4E94\u516D"[Number(wd)]})`, active: cell.active });
1981
+ }
1982
+ }
1983
+ score.sort((a, b) => b.active - a.active);
1984
+ return {
1985
+ daysSampled: state.days.length,
1986
+ peakHours: score.filter((s) => s.active > 0).slice(0, 6).map((s) => s.key)
1987
+ };
1988
+ }
1989
+
1990
+ // src/weekly/collect.ts
1991
+ import fs9 from "fs";
1992
+ var DAY_MS2 = 864e5;
1993
+ var WINDOW_DAYS = 7;
1994
+ function inWindow(ts, start, end) {
1995
+ const t = Date.parse(ts);
1996
+ return Number.isFinite(t) && t >= start && t <= end;
1997
+ }
1998
+ function trim(text, max) {
1999
+ const t = text.replace(/\s+/g, " ").trim();
2000
+ return t.length > max ? t.slice(0, max - 1) + "\u2026" : t;
2001
+ }
2002
+ function collectWeeklyFacts(guard, paths, policy, now = Date.now()) {
2003
+ const start = now - WINDOW_DAYS * DAY_MS2;
2004
+ const end = now;
2005
+ let spoken = 0;
2006
+ let silent = 0;
2007
+ let observedItems = 0;
2008
+ let reportMissing = 0;
2009
+ const reasons = /* @__PURE__ */ new Map();
2010
+ try {
2011
+ for (const e of readAuditLines(paths.logsDir + "/heartbeat.jsonl")) {
2012
+ const ts = e.ts ?? "";
2013
+ if (!inWindow(ts, start, end)) continue;
2014
+ const ev = e.event;
2015
+ if (ev === "spoke") spoken += 1;
2016
+ else if (ev === "report_missing") reportMissing += 1;
2017
+ else if (ev === "silent") {
2018
+ silent += 1;
2019
+ const reason = String(e.reason ?? "unknown").slice(0, 60);
2020
+ reasons.set(reason, (reasons.get(reason) ?? 0) + 1);
2021
+ } else if (ev === "observed") {
2022
+ observedItems += Number(e.added ?? 0);
2023
+ }
2024
+ }
2025
+ } catch {
2026
+ }
2027
+ const profileAdds = [];
2028
+ try {
2029
+ const raw = fs9.readFileSync(journalFilePath(paths.dataDir), "utf8");
2030
+ for (const line of raw.split("\n")) {
2031
+ if (!line.trim()) continue;
2032
+ try {
2033
+ const rec = JSON.parse(line);
2034
+ if (!inWindow(rec.ts ?? "", start, end)) continue;
2035
+ for (const op of rec.applied ?? []) {
2036
+ if (op.op !== "ADD" || !op.content) continue;
2037
+ profileAdds.push({
2038
+ partition: String(op.partition ?? "?"),
2039
+ topic: String(op.subTopic ? `${op.topic}/${op.subTopic}` : op.topic ?? "?"),
2040
+ content: trim(op.content, 60)
2041
+ });
2042
+ }
2043
+ } catch {
2044
+ }
2045
+ }
2046
+ } catch {
2047
+ }
2048
+ const ledger = { added: [], done: [], stale: [] };
2049
+ try {
2050
+ const { entries } = readLedger(guard, ledgerFilePath(paths.dataDir));
2051
+ for (const e of entries) {
2052
+ const t = Date.parse(`${e.date}T${e.time}:00`);
2053
+ if (Number.isFinite(t) && t >= start && t <= end) {
2054
+ (e.status === "done" ? ledger.done : ledger.added).push(trim(e.text, 40));
2055
+ }
2056
+ }
2057
+ const staleCutoffDays = 12;
2058
+ for (const e of pendingOlderThan(guard, ledgerFilePath(paths.dataDir), staleCutoffDays, now)) {
2059
+ const days = Math.max(1, Math.round((now - Date.parse(`${e.date}T${e.time}:00`)) / DAY_MS2));
2060
+ ledger.stale.push({ text: trim(e.text, 40), days });
2061
+ }
2062
+ } catch {
2063
+ }
2064
+ const seeds = { added: [], consumed: [], waiting: [], poolActive: 0 };
2065
+ try {
2066
+ const db = loadPool(guard, seedsFilePath(paths.dataDir));
2067
+ seeds.poolActive = activeSeeds(db).length;
2068
+ for (const s of db.seeds) {
2069
+ if (inWindow(s.bornAt, start, end)) seeds.added.push(trim(s.text, 40));
2070
+ if (s.status === "archived" && s.retireReason === "consumed" && s.retiredAt && inWindow(s.retiredAt, start, end)) {
2071
+ seeds.consumed.push(trim(s.text, 40));
2072
+ }
2073
+ }
2074
+ 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));
2075
+ } catch {
2076
+ }
2077
+ const reports = { total: 0, material: 0, heartfelt: 0, silent: 0, missing: reportMissing, reasons: [] };
2078
+ try {
2079
+ const reasonTally = /* @__PURE__ */ new Map();
2080
+ for (const r of readSeedReports(guard, reportFilePath(paths.dataDir))) {
2081
+ if (r.ts < start || r.ts > end) continue;
2082
+ reports.total += 1;
2083
+ if (r.spoken === "material") reports.material += 1;
2084
+ else if (r.spoken === "heartfelt") reports.heartfelt += 1;
2085
+ else reports.silent += 1;
2086
+ if (r.reason) reasonTally.set(r.reason, (reasonTally.get(r.reason) ?? 0) + 1);
2087
+ }
2088
+ reports.reasons = [...reasonTally.entries()].map(([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count);
2089
+ } catch {
2090
+ }
2091
+ let peakHours = [];
2092
+ try {
2093
+ peakHours = summarizeRhythm(paths).peakHours;
2094
+ } catch {
2095
+ }
2096
+ return {
2097
+ windowStart: new Date(start).toISOString(),
2098
+ windowEnd: new Date(end).toISOString(),
2099
+ spoken,
2100
+ silent,
2101
+ silentTopReasons: [...reasons.entries()].map(([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count).slice(0, 3),
2102
+ observedItems,
2103
+ profileAdds,
2104
+ ledger,
2105
+ seeds,
2106
+ reports,
2107
+ peakHours
2108
+ };
2109
+ }
1866
2110
 
1867
2111
  // src/core/orchestrator.ts
1868
2112
  var reschedule = null;
@@ -1877,8 +2121,8 @@ function sessionEvents(session) {
1877
2121
  if (!session) return [];
1878
2122
  if (typeof session.snapshotEvents === "function") {
1879
2123
  try {
1880
- const snapshot2 = session.snapshotEvents();
1881
- if (Array.isArray(snapshot2)) return snapshot2;
2124
+ const snapshot = session.snapshotEvents();
2125
+ if (Array.isArray(snapshot)) return snapshot;
1882
2126
  } catch {
1883
2127
  }
1884
2128
  }
@@ -1915,7 +2159,7 @@ function noteBeat(verdict, detail) {
1915
2159
  lastBeat = { at: (/* @__PURE__ */ new Date()).toISOString(), verdict, ...detail };
1916
2160
  }
1917
2161
  function stateFile(paths) {
1918
- return path9.join(paths.dataDir, "gate.json");
2162
+ return path10.join(paths.dataDir, "gate.json");
1919
2163
  }
1920
2164
  function readBeatState(guard, paths) {
1921
2165
  try {
@@ -1951,6 +2195,15 @@ function defaultAgentOptions(ctx) {
1951
2195
  }
1952
2196
  return void 0;
1953
2197
  }
2198
+ var ENGINE_ROOM_EXTRA_TOOLS = ["compress", "decompress", "acp_status"];
2199
+ function buildEngineRoomAllowList(globalNames, configExtras = []) {
2200
+ const global = new Set(globalNames);
2201
+ const allow = ["web_search"];
2202
+ for (const name2 of [...ENGINE_ROOM_EXTRA_TOOLS, ...configExtras.map((t) => t.trim()).filter(Boolean)]) {
2203
+ if (global.has(name2) && !allow.includes(name2)) allow.push(name2);
2204
+ }
2205
+ return allow;
2206
+ }
1954
2207
  function makeHomeSetup(deps) {
1955
2208
  const { ctx, paths } = deps;
1956
2209
  return async (agentCtx) => {
@@ -1972,7 +2225,12 @@ function makeHomeSetup(deps) {
1972
2225
  if (typeof tools?.restrict !== "function") {
1973
2226
  notes.push("restrict=no-api");
1974
2227
  } else {
1975
- const allow = ["web_search"];
2228
+ let globalNames = [];
2229
+ try {
2230
+ globalNames = (tools.schemas?.() ?? []).map((s) => String(s?.name ?? ""));
2231
+ } catch {
2232
+ }
2233
+ const allow = buildEngineRoomAllowList(globalNames, deps.extraTools ?? []);
1976
2234
  try {
1977
2235
  tools.restrict({ allow });
1978
2236
  notes.push(`restrict=ok allow=${allow.join("|")}`);
@@ -1980,7 +2238,7 @@ function makeHomeSetup(deps) {
1980
2238
  notes.push(`restrict=threw(${String(e).slice(0, 200)})`);
1981
2239
  }
1982
2240
  try {
1983
- const visible = (tools.schemas?.() ?? []).map((s) => String(s?.name ?? "?")).sort();
2241
+ const visible = globalNames.sort();
1984
2242
  notes.push(`visibleGlobal=${visible.length > 0 ? visible.join(",") : "(empty)"}`);
1985
2243
  } catch (e) {
1986
2244
  notes.push(`visibleGlobal=threw(${String(e).slice(0, 60)})`);
@@ -2021,6 +2279,36 @@ async function rotateHome(deps, reason) {
2021
2279
  reason
2022
2280
  });
2023
2281
  ctx.logger.info("heartbeat: home rotated %s -> %s (%s)", old ?? "(none)", realId, reason);
2282
+ if (shouldArchiveRotatedHome({ old, realId, enabled: deps.policy.heartbeat.archiveRotatedHome !== false })) {
2283
+ archiveSessionBestEffort(deps, old, reason);
2284
+ }
2285
+ }
2286
+ function shouldArchiveRotatedHome(input) {
2287
+ return Boolean(input.old) && input.old !== input.realId && input.enabled;
2288
+ }
2289
+ function archiveSessionBestEffort(deps, sessionId, why) {
2290
+ const audit = (entry) => {
2291
+ try {
2292
+ appendAuditLine(deps.paths.logsDir + "/heartbeat.jsonl", entry);
2293
+ } catch {
2294
+ }
2295
+ };
2296
+ try {
2297
+ const injectable = deps.ctx;
2298
+ injectable.inject(["workspaceRegistry"], (scoped) => {
2299
+ const registry = scoped.workspaceRegistry;
2300
+ if (!registry || typeof registry.archiveSession !== "function") {
2301
+ audit({ event: "home_rotate_archive", ok: false, sessionId, why, error: "workspaceRegistry unavailable" });
2302
+ return;
2303
+ }
2304
+ void Promise.resolve(registry.archiveSession(sessionId, { stopActivity: true })).then(
2305
+ () => audit({ event: "home_rotate_archive", ok: true, sessionId, why }),
2306
+ (e) => audit({ event: "home_rotate_archive", ok: false, sessionId, why, error: String(e).slice(0, 160) })
2307
+ );
2308
+ });
2309
+ } catch (e) {
2310
+ audit({ event: "home_rotate_archive", ok: false, sessionId, why, error: String(e).slice(0, 160) });
2311
+ }
2024
2312
  }
2025
2313
  function unwrapHomeHandle(handle) {
2026
2314
  return handle.agent ?? handle;
@@ -2190,12 +2478,21 @@ function parseJsonBlock(raw) {
2190
2478
  }
2191
2479
  throw new Error(text.includes("{") ? "unparseable JSON object in model output" : "no JSON object in model output");
2192
2480
  }
2481
+ function seedAuditSink(paths) {
2482
+ return (entry) => {
2483
+ try {
2484
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", entry);
2485
+ } catch {
2486
+ }
2487
+ };
2488
+ }
2193
2489
  async function maintenancePhase(bc) {
2194
2490
  const { deps, now } = bc;
2195
2491
  const { guard, paths, policy } = deps;
2196
- gcPool(guard, seedsFilePath(paths.dataDir), policy, now);
2492
+ gcPool(guard, seedsFilePath(paths.dataDir), policy, now, seedAuditSink(paths));
2197
2493
  pruneAuditFile(paths.logsDir + "/envpulse.jsonl", policy.retention.envPulseHours * 36e5, now);
2198
2494
  pruneAuditFile(paths.logsDir + "/heartbeat.jsonl", policy.retention.decisionLogDays * 864e5, now);
2495
+ snapshotIfDue(guard, paths.dataDir, paths.logsDir + "/heartbeat.jsonl", "maintenance-threshold", now);
2199
2496
  const cons = shouldConsolidate(guard, paths, policy, now);
2200
2497
  if (cons.due) {
2201
2498
  const llm = async (prompt) => {
@@ -2227,14 +2524,14 @@ async function observeBoundSessions(bc) {
2227
2524
  const { deps, now } = bc;
2228
2525
  const { guard, paths } = deps;
2229
2526
  const { loadBindings: loadBindings2, observeTargets } = await import("./bindings-XPPSKILN.js");
2230
- const { inboxAppend, inboxFilePath: inboxFilePath2 } = await import("./inbox-MMLHISQV.js");
2527
+ const { inboxAppend, inboxFilePath: inboxFilePath2 } = await import("./inbox-57NHQHY3.js");
2231
2528
  const data = loadBindings2(guard, paths.settingsDir);
2232
2529
  const targets = observeTargets(data);
2233
2530
  if (targets.length === 0) return;
2234
- const cursorFile = path9.join(paths.dataDir, "cursors.json");
2531
+ const cursorFile = path10.join(paths.dataDir, "cursors.json");
2235
2532
  let cursors = {};
2236
2533
  try {
2237
- cursors = JSON.parse(fs9.readFileSync(cursorFile, "utf8"));
2534
+ cursors = JSON.parse(fs10.readFileSync(cursorFile, "utf8"));
2238
2535
  } catch {
2239
2536
  }
2240
2537
  const inboxFile = inboxFilePath2(paths.dataDir);
@@ -2246,7 +2543,8 @@ async function observeBoundSessions(bc) {
2246
2543
  const cursor = cursors[b.sessionId] ?? 0;
2247
2544
  let last = cursor;
2248
2545
  let added = 0;
2249
- for (let i = cursor; i < events.length && added < 10; i++) {
2546
+ const { maxChars, perBeat } = deps.policy.observe;
2547
+ for (let i = cursor; i < events.length && added < perBeat; i++) {
2250
2548
  const e = events[i];
2251
2549
  if (e.type !== "user/message") continue;
2252
2550
  const d = e.data;
@@ -2257,7 +2555,7 @@ async function observeBoundSessions(bc) {
2257
2555
  kind: "chat",
2258
2556
  at: new Date(now).toISOString(),
2259
2557
  ref: `cursors.json#${b.sessionId}:${i}`,
2260
- note: text.split(/[。!?\n]/)[0].slice(0, 80)
2558
+ note: text.split(/[。!?\n]/)[0].slice(0, maxChars)
2261
2559
  });
2262
2560
  added += 1;
2263
2561
  last = i + 1;
@@ -2272,6 +2570,53 @@ async function observeBoundSessions(bc) {
2272
2570
  }
2273
2571
  }
2274
2572
  }
2573
+ async function weeklyPhase(bc) {
2574
+ const { deps, agent, now } = bc;
2575
+ const { guard, paths, policy } = deps;
2576
+ if (policy.weekly.enabled === false) return;
2577
+ const audit = (entry) => {
2578
+ try {
2579
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", entry);
2580
+ } catch {
2581
+ }
2582
+ };
2583
+ if (ensureWeeklyAnchor(guard, paths.dataDir, now)) {
2584
+ audit({ event: "weekly_anchored" });
2585
+ }
2586
+ if (!weeklyDue(guard, paths.dataDir, now)) return;
2587
+ if (!agent) {
2588
+ audit({ event: "weekly_skipped", reason: "agentless beat" });
2589
+ return;
2590
+ }
2591
+ try {
2592
+ const facts = collectWeeklyFacts(guard, paths, policy, now);
2593
+ let text = "";
2594
+ let source = "template";
2595
+ try {
2596
+ text = (await agentTurn(deps, agent, buildWeeklyPrompt(facts), "weekly", IDLE_WAIT_TIMEOUT_MS)).trim();
2597
+ if (text) source = "llm";
2598
+ } catch (e) {
2599
+ audit({ event: "weekly_llm_failed", error: String(e).slice(0, 160) });
2600
+ }
2601
+ if (!text) text = renderTemplateReport(facts);
2602
+ saveWeeklyReport(guard, paths.dataDir, {
2603
+ start: facts.windowStart,
2604
+ end: facts.windowEnd,
2605
+ generatedAt: new Date(now).toISOString(),
2606
+ source,
2607
+ text
2608
+ });
2609
+ audit({ event: "weekly_generated", source, chars: text.length });
2610
+ if (!inQuietHours(policy, now)) {
2611
+ try {
2612
+ sendWeeklyReadyHint(paths);
2613
+ } catch {
2614
+ }
2615
+ }
2616
+ } catch (e) {
2617
+ audit({ event: "weekly_failed", error: String(e).slice(0, 160) });
2618
+ }
2619
+ }
2275
2620
  function ctx_getAgent(deps, sessionId) {
2276
2621
  try {
2277
2622
  const agent = deps.ctx.agents.get(sessionId);
@@ -2364,7 +2709,7 @@ async function runWanderTurn(bc, prompt, focus, opts) {
2364
2709
  tag: "news",
2365
2710
  source: "browse",
2366
2711
  confidence: 0.4
2367
- }, now);
2712
+ }, now, seedAuditSink(paths));
2368
2713
  registered += 1;
2369
2714
  }
2370
2715
  } catch (e) {
@@ -2374,6 +2719,24 @@ async function runWanderTurn(bc, prompt, focus, opts) {
2374
2719
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: opts.label, focus, registered });
2375
2720
  return true;
2376
2721
  }
2722
+ function recordDeliveryFromAccount(deps, materials, account) {
2723
+ if (account.spoken === "silent" && account.reason !== "\u7D20\u6750\u4E0D\u642D") return;
2724
+ const { guard, paths } = deps;
2725
+ const db = loadPool(guard, seedsFilePath(paths.dataDir));
2726
+ const doc = loadProfile(guard, profileFilePath(paths.dataDir));
2727
+ const seedTopic = new Map(db.seeds.map((s) => [s.id, s.topic]));
2728
+ const entryTopic = new Map(
2729
+ [...doc.partitions.interest.entries, ...doc.partitions.projects.entries].map((e) => [e.id, `${e.topic}/${e.subTopic}`])
2730
+ );
2731
+ const topicOf = (id) => seedTopic.get(id) ?? entryTopic.get(id) ?? null;
2732
+ const offered = [...new Set(materials.map((m) => topicOf(m.id)).filter((t) => Boolean(t)))];
2733
+ const adopted = [...new Set([...account.seedIds, ...account.profileIds].map(topicOf).filter((t) => Boolean(t)))];
2734
+ recordDelivery(guard, preferenceFilePath(paths.dataDir), {
2735
+ offeredTopics: offered,
2736
+ adoptedTopics: adopted,
2737
+ now: Date.now()
2738
+ });
2739
+ }
2377
2740
  async function deliverPackage(bc, materials, opts) {
2378
2741
  const { deps, now } = bc;
2379
2742
  const { guard, paths, policy } = deps;
@@ -2404,11 +2767,14 @@ async function deliverPackage(bc, materials, opts) {
2404
2767
  }
2405
2768
  const voiceSessionId = voiceAgent.session?.id ?? null;
2406
2769
  try {
2770
+ const deliveryId = `d${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
2407
2771
  const phrasePrompt = buildMaterialPrompt(materials, {
2772
+ deliveryId,
2408
2773
  // spec ②: the "我在干嘛" line rides on every delivery when vision
2409
2774
  // produced one this beat; absent otherwise (never invented).
2410
2775
  ...typeof opts.doing === "string" && opts.doing.trim() ? { doing: opts.doing.trim().slice(0, 80) } : {}
2411
2776
  });
2777
+ const turnStart = Date.now();
2412
2778
  let spokenRaw;
2413
2779
  try {
2414
2780
  spokenRaw = await agentTurn(bc.deps, voiceAgent, phrasePrompt, "expression", EXPRESSION_IDLE_WAIT_MS);
@@ -2424,9 +2790,31 @@ async function deliverPackage(bc, materials, opts) {
2424
2790
  const spokenLines = spokenRaw.replace(/<\/?thinking[\s\S]*?<\/think>/gi, "").trim().split("\n").map((l) => l.trim()).filter((l) => l && !/^<\/?tool_calls?>$/i.test(l));
2425
2791
  const cnLine = [...spokenLines].reverse().find((l) => /[\u4e00-\u9fff]/.test(l));
2426
2792
  const text = (cnLine ?? spokenLines[spokenLines.length - 1] ?? "").slice(0, 200);
2427
- if (!text || !/[\u4e00-\u9fff]/.test(text)) {
2428
- appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "spoke_failed", reason: "non-Chinese output discarded" });
2429
- noteBeat("spoke_failed", { reason: "non-Chinese output discarded" });
2793
+ const spokeText = Boolean(text && /[\u4e00-\u9fff]/.test(text));
2794
+ let report = null;
2795
+ try {
2796
+ const reports = readSeedReportsSince(guard, reportFilePath(paths.dataDir), turnStart);
2797
+ report = pickReport(reports, deliveryId);
2798
+ } catch (e) {
2799
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "report_read_failed", error: String(e).slice(0, 120) });
2800
+ }
2801
+ const account = reconcileDelivery(materials, report, opts.seedIds, spokeText);
2802
+ if (!spokeText) {
2803
+ if (account.source === "report" && account.spoken === "silent") {
2804
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
2805
+ event: "silent",
2806
+ reason: `\u62A5\u8D26\u6C89\u9ED8:${account.reason ?? "\u672A\u8BF4\u660E"}`
2807
+ });
2808
+ noteBeat("silent", { reason: "\u672C\u4EBA\u62A5\u8D26\u6C89\u9ED8" });
2809
+ try {
2810
+ recordDeliveryFromAccount(deps, materials, account);
2811
+ } catch (e) {
2812
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "preference_record_failed", error: String(e).slice(0, 120) });
2813
+ }
2814
+ } else {
2815
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "spoke_failed", reason: "non-Chinese output discarded" });
2816
+ noteBeat("spoke_failed", { reason: "non-Chinese output discarded" });
2817
+ }
2430
2818
  return;
2431
2819
  }
2432
2820
  const confirm = confirmSend(guard, policy, paths, "topic", text, now);
@@ -2435,12 +2823,31 @@ async function deliverPackage(bc, materials, opts) {
2435
2823
  noteBeat("spoke_failed", { reason: confirm.reason });
2436
2824
  return;
2437
2825
  }
2438
- const usedIds = new Set(attributionIds(materials, text, opts.seedIds));
2439
- for (const id of usedIds) {
2440
- surfaceSeed(guard, seedsFilePath(paths.dataDir), policy, id, now);
2826
+ for (const id of account.seedIds) {
2827
+ surfaceSeed(guard, seedsFilePath(paths.dataDir), policy, id, now, seedAuditSink(paths));
2828
+ }
2829
+ if (account.source === "none") {
2830
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
2831
+ event: "report_missing",
2832
+ offered: materials.map((m) => m.id).join(",")
2833
+ });
2834
+ } else {
2835
+ try {
2836
+ recordDeliveryFromAccount(deps, materials, account);
2837
+ } catch (e) {
2838
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "preference_record_failed", error: String(e).slice(0, 120) });
2839
+ }
2441
2840
  }
2442
2841
  sendNewMessageHint(paths);
2443
- appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "spoke", text: text.slice(0, 80), seeds: [...usedIds] });
2842
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
2843
+ event: "spoke",
2844
+ text: text.slice(0, 80),
2845
+ seeds: account.seedIds,
2846
+ profile_ids: account.profileIds,
2847
+ source: account.source,
2848
+ spoken: account.spoken,
2849
+ ...account.reason ? { reason: account.reason } : {}
2850
+ });
2444
2851
  if (voiceSessionId && voiceSessionId !== homeId) {
2445
2852
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "delivered", sessionId: voiceSessionId });
2446
2853
  }
@@ -2479,9 +2886,13 @@ async function expressionPhases(bc) {
2479
2886
  const offered = assembleCandidates(activeSeeds(loadPool(guard, seedsFilePath(paths.dataDir))));
2480
2887
  if (offered.length === 0) {
2481
2888
  if (policy.heartbeat.idleMode && bc.agent) {
2482
- const idleTopics = digest.topic.trim();
2483
- if (idleTopics) {
2484
- const fallback = idleTopics.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 3).map((line, i) => ({ id: "idle-" + i, text: line.slice(0, 60), used: 0 }));
2889
+ const idleEntries = profileTopicEntries(loadProfile(guard, profileFilePath(paths.dataDir)), 3);
2890
+ if (idleEntries.length > 0) {
2891
+ const fallback = idleEntries.map((e) => ({
2892
+ id: e.id,
2893
+ text: `${e.partition === "projects" ? "\u8FDB\u884C\u4E2D" : "\u5174\u8DA3"} ${e.topic}/${e.subTopic}: ${e.content}`.slice(0, 60),
2894
+ used: 0
2895
+ }));
2485
2896
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "idle_fallback", topics: fallback.length });
2486
2897
  return deliverPackage(bc, fallback, {
2487
2898
  seedIds: [],
@@ -2632,6 +3043,7 @@ async function beat(deps) {
2632
3043
  } else {
2633
3044
  await maintenancePhase(bc);
2634
3045
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "phase_done", phase: "maintenance" });
3046
+ await weeklyPhase(bc);
2635
3047
  await collectPhase(bc);
2636
3048
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "phase_done", phase: "collect" });
2637
3049
  wandered = await wanderPhase(bc);
@@ -2711,26 +3123,26 @@ function startOrchestrator(deps) {
2711
3123
 
2712
3124
  // src/rpc.ts
2713
3125
  import { spawn as spawn2 } from "child_process";
2714
- import fs12 from "fs";
3126
+ import fs13 from "fs";
2715
3127
  import os from "os";
2716
- import path12 from "path";
3128
+ import path13 from "path";
2717
3129
 
2718
3130
  // src/browse/interests-edit.ts
2719
- import fs10 from "fs";
2720
- import path10 from "path";
3131
+ import fs11 from "fs";
3132
+ import path11 from "path";
2721
3133
  var MAX_INTERESTS = 32;
2722
3134
  var MAX_INTEREST_LEN = 60;
2723
3135
  var MAX_WINDOWS = 6;
2724
3136
  var HHMM = /^([01]\d|2[0-3]):([0-5]\d)$/;
2725
3137
  function userInterestsPath(paths) {
2726
- return path10.join(paths.settingsDir, "interests.json");
3138
+ return path11.join(paths.settingsDir, "interests.json");
2727
3139
  }
2728
3140
  function factoryInterestsPath(paths) {
2729
- return path10.join(paths.configDir, "interests.json");
3141
+ return path11.join(paths.configDir, "interests.json");
2730
3142
  }
2731
3143
  function readDoc(file) {
2732
3144
  try {
2733
- const raw = JSON.parse(fs10.readFileSync(file, "utf8"));
3145
+ const raw = JSON.parse(fs11.readFileSync(file, "utf8"));
2734
3146
  if (!Array.isArray(raw.interests)) return null;
2735
3147
  return { ...raw, interests: raw.interests.map(String) };
2736
3148
  } catch {
@@ -2748,8 +3160,8 @@ function ensureUserLayer(guard, paths) {
2748
3160
  return doc;
2749
3161
  }
2750
3162
  function saveDoc(guard, file, doc) {
2751
- fs10.mkdirSync(path10.dirname(file), { recursive: true });
2752
- fs10.writeFileSync(guard.assert(file), JSON.stringify(doc, null, 2), "utf8");
3163
+ fs11.mkdirSync(path11.dirname(file), { recursive: true });
3164
+ fs11.writeFileSync(guard.assert(file), JSON.stringify(doc, null, 2), "utf8");
2753
3165
  }
2754
3166
  function normalizeInterest(text) {
2755
3167
  return text.trim().replace(/\s+/g, " ");
@@ -2815,8 +3227,8 @@ function setWanderWindows(guard, paths, rawWindows) {
2815
3227
  }
2816
3228
 
2817
3229
  // src/statusbar/time-inject.ts
2818
- import fs11 from "fs";
2819
- import path11 from "path";
3230
+ import fs12 from "fs";
3231
+ import path12 from "path";
2820
3232
  function shouldInjectTime(input) {
2821
3233
  if (input.step !== 1) return false;
2822
3234
  if (!input.originIsInboxSplice) return false;
@@ -2878,11 +3290,11 @@ function lastMessageTime(session) {
2878
3290
  return void 0;
2879
3291
  }
2880
3292
  function timeInjectStatePath(dataDir) {
2881
- return path11.join(dataDir, "time-inject-state.json");
3293
+ return path12.join(dataDir, "time-inject-state.json");
2882
3294
  }
2883
3295
  function loadTimeInjectState(guard, dataDir) {
2884
3296
  try {
2885
- const parsed = JSON.parse(fs11.readFileSync(guard.assert(timeInjectStatePath(dataDir)), "utf8"));
3297
+ const parsed = JSON.parse(fs12.readFileSync(guard.assert(timeInjectStatePath(dataDir)), "utf8"));
2886
3298
  return parsed && typeof parsed === "object" ? parsed : {};
2887
3299
  } catch {
2888
3300
  return {};
@@ -2947,7 +3359,7 @@ var cachedVersion = null;
2947
3359
  function pluginVersion(paths) {
2948
3360
  if (cachedVersion) return cachedVersion;
2949
3361
  try {
2950
- const pkg = JSON.parse(fs12.readFileSync(path12.join(paths.packageRoot, "package.json"), "utf8"));
3362
+ const pkg = JSON.parse(fs13.readFileSync(path13.join(paths.packageRoot, "package.json"), "utf8"));
2951
3363
  cachedVersion = typeof pkg.version === "string" ? pkg.version : "unknown";
2952
3364
  } catch {
2953
3365
  cachedVersion = "unknown";
@@ -2956,12 +3368,20 @@ function pluginVersion(paths) {
2956
3368
  }
2957
3369
  function homeSessionId(paths, guard) {
2958
3370
  try {
2959
- const raw = loadEncryptedText(guard, path12.join(paths.dataDir, "gate.json"));
3371
+ const raw = loadEncryptedText(guard, path13.join(paths.dataDir, "gate.json"));
2960
3372
  return JSON.parse(raw ?? "{}").sessionId ?? null;
2961
3373
  } catch {
2962
3374
  return null;
2963
3375
  }
2964
3376
  }
3377
+ function seedAuditSink2(paths) {
3378
+ return (entry) => {
3379
+ try {
3380
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", entry);
3381
+ } catch {
3382
+ }
3383
+ };
3384
+ }
2965
3385
  function auditInterests(paths, action, result, detail) {
2966
3386
  if (!result.ok) return;
2967
3387
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
@@ -2978,13 +3398,13 @@ function loadSessionTitles() {
2978
3398
  if (row && typeof row.val === "string" && row.val && !titles[id]) titles[id] = row.val;
2979
3399
  };
2980
3400
  try {
2981
- const dir = path12.join(os.homedir(), ".dsh", "storages", "session_projcache", "sessions");
2982
- for (const file of fs12.readdirSync(dir)) {
3401
+ const dir = path13.join(os.homedir(), ".dsh", "storages", "session_projcache", "sessions");
3402
+ for (const file of fs13.readdirSync(dir)) {
2983
3403
  if (!file.endsWith(".json")) continue;
2984
3404
  const id = file.slice(0, -".json".length);
2985
3405
  if (!id.startsWith("session-")) continue;
2986
3406
  try {
2987
- const record = JSON.parse(fs12.readFileSync(path12.join(dir, file), "utf8"));
3407
+ const record = JSON.parse(fs13.readFileSync(path13.join(dir, file), "utf8"));
2988
3408
  take(id, record.record);
2989
3409
  } catch {
2990
3410
  }
@@ -2992,7 +3412,7 @@ function loadSessionTitles() {
2992
3412
  } catch {
2993
3413
  }
2994
3414
  try {
2995
- const raw = JSON.parse(fs12.readFileSync(path12.join(os.homedir(), ".dsh", "storages", "session_projcache.json"), "utf8"));
3415
+ const raw = JSON.parse(fs13.readFileSync(path13.join(os.homedir(), ".dsh", "storages", "session_projcache.json"), "utf8"));
2996
3416
  const walk = (node) => {
2997
3417
  if (!node || typeof node !== "object") return;
2998
3418
  for (const [key, value] of Object.entries(node)) {
@@ -3010,8 +3430,8 @@ function loadSessionTitles() {
3010
3430
  }
3011
3431
  function loadVisibleSessionIds(registryFile) {
3012
3432
  try {
3013
- const file = registryFile ?? path12.join(os.homedir(), ".dsh", "storages", "workspace.json");
3014
- const j = JSON.parse(fs12.readFileSync(file, "utf8"));
3433
+ const file = registryFile ?? path13.join(os.homedir(), ".dsh", "storages", "workspace.json");
3434
+ const j = JSON.parse(fs13.readFileSync(file, "utf8"));
3015
3435
  const archived = new Set(
3016
3436
  (Array.isArray(j.global?.archivedSessionIds) ? j.global.archivedSessionIds : []).map((s) => String(s))
3017
3437
  );
@@ -3082,15 +3502,15 @@ function installHeartbeatRpc(ctx, deps) {
3082
3502
  });
3083
3503
  }
3084
3504
  case "sessions.list": {
3085
- const root = path12.join(os.homedir(), ".dsh", "sessions");
3505
+ const root = path13.join(os.homedir(), ".dsh", "sessions");
3086
3506
  const bindings = loadBindings(guard, paths.settingsDir).bindings;
3087
3507
  const home = homeSessionId(paths, guard);
3088
3508
  const titles = loadSessionTitles();
3089
3509
  const { visible, registryFound } = loadVisibleSessionIds();
3090
3510
  const out = [];
3091
- if (fs12.existsSync(root)) {
3092
- for (const slug of fs12.readdirSync(root)) {
3093
- for (const id of fs12.readdirSync(path12.join(root, slug))) {
3511
+ if (fs13.existsSync(root)) {
3512
+ for (const slug of fs13.readdirSync(root)) {
3513
+ for (const id of fs13.readdirSync(path13.join(root, slug))) {
3094
3514
  if (registryFound && id !== home && !visible.has(id)) continue;
3095
3515
  const binding = bindings.find((b) => b.sessionId === id);
3096
3516
  out.push({
@@ -3126,7 +3546,7 @@ function installHeartbeatRpc(ctx, deps) {
3126
3546
  let homeReset = false;
3127
3547
  if (id === homeSessionId(paths, guard)) {
3128
3548
  try {
3129
- fs12.rmSync(guard.assert(path12.join(paths.dataDir, "gate.json")), { force: true });
3549
+ fs13.rmSync(guard.assert(path13.join(paths.dataDir, "gate.json")), { force: true });
3130
3550
  homeReset = true;
3131
3551
  } catch {
3132
3552
  }
@@ -3142,15 +3562,15 @@ function installHeartbeatRpc(ctx, deps) {
3142
3562
  });
3143
3563
  }
3144
3564
  case "seeds.archive": {
3145
- const s = archiveSeedById(guard, seedsFilePath(paths.dataDir), String(p.id ?? ""), "completed");
3565
+ const s = archiveSeedById(guard, seedsFilePath(paths.dataDir), String(p.id ?? ""), "completed", Date.now(), seedAuditSink2(paths));
3146
3566
  return s ? ok(s) : err("not-found", "active seed not found");
3147
3567
  }
3148
3568
  case "seeds.restore": {
3149
- const r = restoreSeed(guard, seedsFilePath(paths.dataDir), policy, String(p.id ?? ""));
3569
+ const r = restoreSeed(guard, seedsFilePath(paths.dataDir), policy, String(p.id ?? ""), Date.now(), seedAuditSink2(paths));
3150
3570
  return r.ok ? ok(r.seed) : err("restore-failed", r.reason);
3151
3571
  }
3152
3572
  case "seeds.delete": {
3153
- return ok({ deleted: deleteSeed(guard, seedsFilePath(paths.dataDir), String(p.id ?? "")) });
3573
+ return ok({ deleted: deleteSeed(guard, seedsFilePath(paths.dataDir), String(p.id ?? ""), seedAuditSink2(paths)) });
3154
3574
  }
3155
3575
  // ── 兴趣范围 / 浏览时段(v1.4.0;首编继承出厂,见 interests-edit.ts)──
3156
3576
  case "interests.list":
@@ -3208,16 +3628,61 @@ function installHeartbeatRpc(ctx, deps) {
3208
3628
  lines.push(`- [${e.topic}/${e.subTopic}] ${e.content} (conf ${e.confidence.toFixed(2)}, ${e.temporal})`);
3209
3629
  }
3210
3630
  }
3211
- const out = path12.join(paths.exportsDir, `profile-export-${Date.now()}.md`);
3631
+ const out = path13.join(paths.exportsDir, `profile-export-${Date.now()}.md`);
3212
3632
  writeText(guard, out, lines.join("\n") + "\n");
3213
3633
  return ok({ path: out });
3214
3634
  }
3215
3635
  case "ledger.open": {
3216
3636
  const f = ledgerFilePath(paths.dataDir);
3217
- if (!fs12.existsSync(guard.assert(f))) fs12.writeFileSync(f, "# \u8D26\u672C\n", "utf8");
3637
+ if (!fs13.existsSync(guard.assert(f))) fs13.writeFileSync(f, "# \u8D26\u672C\n", "utf8");
3218
3638
  spawn2("cmd", ["/c", "start", "", f], { detached: true, stdio: "ignore" }).unref();
3219
3639
  return ok({ path: f });
3220
3640
  }
3641
+ case "weekly.list":
3642
+ return ok({ reports: listWeeklyReports(guard, paths.dataDir) });
3643
+ case "weekly.get": {
3644
+ const file = String(p.file ?? "");
3645
+ const rep = readWeeklyReport(guard, paths.dataDir, file);
3646
+ if (!rep) return err("bad-request", `no such report: ${file}`);
3647
+ return ok(rep);
3648
+ }
3649
+ case "migrate.export": {
3650
+ const passphrase = String(p.passphrase ?? "");
3651
+ if (!passphrase) return err("bad-request", "\u9700\u8981\u8BBE\u7F6E\u53E3\u4EE4");
3652
+ const { collectMigrationEntries, encryptContainer } = await import("./migrate-N6NXMZ3G.js");
3653
+ const { entries } = collectMigrationEntries(guard, paths);
3654
+ if (entries.length === 0) return err("bad-request", "\u6CA1\u6709\u53EF\u6253\u5305\u7684\u8BB0\u5FC6\u6587\u4EF6\uFF08data/ \u662F\u7A7A\u7684\uFF09");
3655
+ const out = path13.join(paths.exportsDir, `heartbeat-memory-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.hbmig`);
3656
+ writeText(guard, out, encryptContainer(entries, passphrase));
3657
+ appendAuditLine(guard.assert(paths.logsDir + "/heartbeat.jsonl"), { event: "migrate_export", files: entries.length });
3658
+ return ok({ path: out, count: entries.length });
3659
+ }
3660
+ case "migrate.import": {
3661
+ const passphrase = String(p.passphrase ?? "");
3662
+ const file = String(p.file ?? "");
3663
+ if (!passphrase || !file) return err("bad-request", "\u9700\u8981\u5BB9\u5668\u8DEF\u5F84\u4E0E\u53E3\u4EE4");
3664
+ const { decryptContainer, applyMigrationEntries } = await import("./migrate-N6NXMZ3G.js");
3665
+ let text;
3666
+ try {
3667
+ text = fs13.readFileSync(file, "utf8");
3668
+ } catch {
3669
+ return err("bad-request", `\u8BFB\u4E0D\u5230\u8FC1\u79FB\u5305\uFF1A${file}`);
3670
+ }
3671
+ let container;
3672
+ try {
3673
+ container = decryptContainer(text, passphrase);
3674
+ } catch (e) {
3675
+ return err("bad-request", String(e instanceof Error ? e.message : e));
3676
+ }
3677
+ const result = applyMigrationEntries(guard, paths, container.files);
3678
+ appendAuditLine(guard.assert(paths.logsDir + "/heartbeat.jsonl"), {
3679
+ event: "migrate_import",
3680
+ restored: result.restored.length,
3681
+ backedUp: result.backedUp.length,
3682
+ skipped: result.skipped.length
3683
+ });
3684
+ return ok(result);
3685
+ }
3221
3686
  default:
3222
3687
  return err("bad-request", `unknown endpoint ${JSON.stringify(endpoint)}`);
3223
3688
  }
@@ -3348,6 +3813,73 @@ function registerStatusbarSection(ctx, guard, paths, opts) {
3348
3813
  });
3349
3814
  }
3350
3815
 
3816
+ // src/ledger/tool.ts
3817
+ var MAX_TEXT = 120;
3818
+ function clean(v) {
3819
+ return String(v ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_TEXT);
3820
+ }
3821
+ function fmtEntry2(e) {
3822
+ return `#${e.id} [${e.status}] ${e.date} ${e.text}`;
3823
+ }
3824
+ function buildLedgerTool(guard, file) {
3825
+ return {
3826
+ name: "ledger",
3827
+ 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',
3828
+ parameters: {
3829
+ type: "object",
3830
+ properties: {
3831
+ action: { type: "string", enum: ["add", "list", "done"], description: "add=\u767B\u8BB0\uFF0Clist=\u67E5\u672A\u5B8C\u6210\uFF0Cdone=\u52FE\u6389" },
3832
+ text: { type: "string", description: "action=add \u65F6\u5FC5\u586B\uFF1A\u4E8B\u9879\u5185\u5BB9\uFF08\u4E00\u53E5\u8BDD\uFF09" },
3833
+ key: { type: "string", description: "action=done \u65F6\u5FC5\u586B\uFF1A#id \u6216\u4E8B\u9879\u6587\u672C\u7684\u552F\u4E00\u5B50\u4E32" }
3834
+ },
3835
+ required: ["action"],
3836
+ additionalProperties: false
3837
+ },
3838
+ output: {
3839
+ schema: {
3840
+ type: "object",
3841
+ properties: {
3842
+ ok: { type: "boolean" },
3843
+ message: { type: "string" }
3844
+ },
3845
+ required: ["ok", "message"],
3846
+ additionalProperties: false
3847
+ },
3848
+ render: (_args, value) => {
3849
+ const v = value;
3850
+ return [{ type: "text", text: String(v.message ?? "") }];
3851
+ }
3852
+ },
3853
+ timeoutMs: 5e3,
3854
+ // Ledger writes go through read-modify-write on one file; never parallel.
3855
+ isConcurrencySafe: () => false,
3856
+ async execute(args) {
3857
+ const a = args ?? {};
3858
+ const action = String(a.action ?? "");
3859
+ if (action === "add") {
3860
+ const text = clean(a.text);
3861
+ if (!text) return { ok: false, message: "add \u9700\u8981\u975E\u7A7A text" };
3862
+ const e = appendEntry(guard, file, text);
3863
+ return { ok: true, message: `\u5DF2\u767B\u8BB0 ${fmtEntry2(e)}` };
3864
+ }
3865
+ if (action === "done") {
3866
+ const key = clean(a.key);
3867
+ if (!key) return { ok: false, message: "done \u9700\u8981 key\uFF08#id \u6216\u6587\u672C\u5B50\u4E32\uFF09" };
3868
+ const e = markDone(guard, file, key);
3869
+ 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` };
3870
+ return { ok: true, message: `\u5DF2\u5B8C\u6210 ${fmtEntry2(e)}` };
3871
+ }
3872
+ if (action === "list") {
3873
+ const items = scanPending(guard, file).map(fmtEntry2);
3874
+ return { ok: true, message: items.length > 0 ? `\u5F53\u524D\u672A\u5B8C\u6210 ${items.length} \u9879\uFF1A
3875
+ ${items.join("\n")}` : "\u8D26\u672C\u91CC\u6CA1\u6709\u672A\u5B8C\u6210\u4E8B\u9879" };
3876
+ }
3877
+ const total = readLedger(guard, file).entries.length;
3878
+ return { ok: false, message: `\u672A\u77E5 action\uFF0C\u8D26\u672C\u5171 ${total} \u6761` };
3879
+ }
3880
+ };
3881
+ }
3882
+
3351
3883
  // src/index.ts
3352
3884
  var name = "heartbeat";
3353
3885
  var inject = ["agents", "settings"];
@@ -3379,7 +3911,13 @@ var Config = Schema.object({
3379
3911
  /** v1.6.3 闲着模式:素材池为空时用画像话题兜底主动搭话(默认关)。 */
3380
3912
  idleMode: Schema.boolean().default(false),
3381
3913
  /** v1.7.0 节省 token 模式:用户离开(闲置 ≥30 分钟)或锁屏时整跳暂停(默认关)。 */
3382
- tokenSaver: Schema.boolean().default(false)
3914
+ tokenSaver: Schema.boolean().default(false),
3915
+ /** v1.8.0 账本工具:向所有日常会话 agent 注册共享账本工具(默认开)。 */
3916
+ ledgerTool: Schema.boolean().default(true),
3917
+ /** v1.9.0 素材报账工具:投递后由陪伴 agent 主动报账(默认开)。 */
3918
+ seedReportTool: Schema.boolean().default(true),
3919
+ /** v1.8.0 引擎室追加工具:逗号分隔的全局工具名,存在才加入白名单(bili 压缩工具自动探测,无需手填)。 */
3920
+ extraTools: Schema.string().default("")
3383
3921
  });
3384
3922
  function apply(ctx, config = {}) {
3385
3923
  const paths = initWorkspace(config.dataDir ? { dataDir: config.dataDir } : {});
@@ -3398,7 +3936,14 @@ function apply(ctx, config = {}) {
3398
3936
  if (config.maxDailySend && config.maxDailySend >= 1) {
3399
3937
  policy = { ...policy, gate: { ...policy.gate, maxDailySend: config.maxDailySend } };
3400
3938
  }
3401
- const deps = { ctx, paths, guard, policy, agentPreset: config.agentPreset || "heartbeat" };
3939
+ const deps = {
3940
+ ctx,
3941
+ paths,
3942
+ guard,
3943
+ policy,
3944
+ agentPreset: config.agentPreset || "heartbeat",
3945
+ extraTools: (config.extraTools ?? "").split(",").map((s) => s.trim()).filter(Boolean)
3946
+ };
3402
3947
  setRuntime({
3403
3948
  paths,
3404
3949
  guard,
@@ -3527,6 +4072,42 @@ function apply(ctx, config = {}) {
3527
4072
  if (typeof merged.tokenSaver === "boolean") tokenSaverRef = merged.tokenSaver;
3528
4073
  };
3529
4074
  installHeartbeatRpc(ctx, { paths, guard, policy, ui: { get: uiGet, set: uiSet } });
4075
+ if (config.ledgerTool !== false) {
4076
+ ctx.inject(["tools"], (scoped) => {
4077
+ const audit = (entry) => {
4078
+ try {
4079
+ appendAuditLine(guard.assert(paths.logsDir + "/heartbeat.jsonl"), entry);
4080
+ } catch {
4081
+ }
4082
+ };
4083
+ try {
4084
+ const tools = scoped.tools;
4085
+ if (!tools || typeof tools.register !== "function") throw new Error("ToolRuntime.register unavailable on this host");
4086
+ tools.register(buildLedgerTool(guard, ledgerFilePath(paths.dataDir)));
4087
+ audit({ event: "ledger_tool_registered" });
4088
+ } catch (e) {
4089
+ audit({ event: "ledger_tool_register_failed", error: String(e).slice(0, 160) });
4090
+ }
4091
+ });
4092
+ }
4093
+ if (config.seedReportTool !== false) {
4094
+ ctx.inject(["tools"], (scoped) => {
4095
+ const audit = (entry) => {
4096
+ try {
4097
+ appendAuditLine(guard.assert(paths.logsDir + "/heartbeat.jsonl"), entry);
4098
+ } catch {
4099
+ }
4100
+ };
4101
+ try {
4102
+ const tools = scoped.tools;
4103
+ if (!tools || typeof tools.register !== "function") throw new Error("ToolRuntime.register unavailable on this host");
4104
+ tools.register(buildSeedReportTool(guard, reportFilePath(paths.dataDir)));
4105
+ audit({ event: "seed_report_tool_registered" });
4106
+ } catch (e) {
4107
+ audit({ event: "seed_report_tool_register_failed", error: String(e).slice(0, 160) });
4108
+ }
4109
+ });
4110
+ }
3530
4111
  startOrchestrator(deps);
3531
4112
  const statusReader = new StatusReader();
3532
4113
  registerStatusbarSection(ctx, guard, paths, {