@kanadego/dsh-heartbeat 1.8.0 → 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
@@ -8,14 +8,17 @@ import {
8
8
  deepMerge,
9
9
  describeInstall,
10
10
  ensureRegistered,
11
+ ensureWeeklyAnchor,
11
12
  installBundledPreset,
12
13
  ledgerFilePath,
13
14
  listWeeklyReports,
14
15
  loadPolicy,
15
16
  markDone,
16
17
  pendingOlderThan,
18
+ preferenceFilePath,
17
19
  readLedger,
18
20
  readWeeklyReport,
21
+ recordDelivery,
19
22
  renderTemplateReport,
20
23
  saveWeeklyReport,
21
24
  scanPending,
@@ -24,7 +27,7 @@ import {
24
27
  updateUserPolicy,
25
28
  userPresetRoot,
26
29
  weeklyDue
27
- } from "./chunk-SQJLKP7L.js";
30
+ } from "./chunk-2SYXKKZF.js";
28
31
  import {
29
32
  getRuntime,
30
33
  setRuntime
@@ -40,7 +43,7 @@ import {
40
43
  restoreSeed,
41
44
  seedsFilePath,
42
45
  surfaceSeed
43
- } from "./chunk-MXJPX3FM.js";
46
+ } from "./chunk-PQTSXW4F.js";
44
47
  import {
45
48
  dedupeItems,
46
49
  inboxClear,
@@ -81,7 +84,7 @@ import {
81
84
  writeText
82
85
  } from "./chunk-IFTFDHZX.js";
83
86
 
84
- // 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
85
88
  function isNullable(value) {
86
89
  return value === null || value === void 0;
87
90
  }
@@ -100,32 +103,6 @@ function pick(source, keys, forced) {
100
103
  for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
101
104
  return result;
102
105
  }
103
- var write = /* @__PURE__ */ Symbol.for("cosmokit.volatile.write");
104
- function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
105
- if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
106
- if (value === null || typeof value !== "object") return value;
107
- if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
108
- ancestors.add(value);
109
- try {
110
- if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
111
- if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
112
- return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
113
- } finally {
114
- ancestors.delete(value);
115
- }
116
- }
117
- function createVolatile(value) {
118
- let current = snapshot(value);
119
- return Object.freeze({
120
- get: () => current,
121
- [write]: (value2) => {
122
- current = value2;
123
- }
124
- });
125
- }
126
- function isVolatile(value) {
127
- return typeof value === "object" && value !== null && write in value;
128
- }
129
106
  function is(type, value) {
130
107
  if (arguments.length === 1) return (value2) => is(type, value2);
131
108
  return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
@@ -204,37 +181,24 @@ function clone(source, refs = /* @__PURE__ */ new Map()) {
204
181
  return result;
205
182
  }
206
183
  function deepEqual(a, b, strict) {
207
- const ancestors = /* @__PURE__ */ new Set();
208
- function compare(a2, b2) {
209
- if (a2 === b2) return true;
210
- if (isVolatile(a2) || isVolatile(b2)) return isVolatile(a2) && isVolatile(b2);
211
- if (!strict && isNullable(a2) && isNullable(b2)) return true;
212
- if (typeof a2 !== typeof b2 || typeof a2 !== "object" || !a2 || !b2) return false;
213
- if (ancestors.has(a2)) return false;
214
- function check(test, then) {
215
- return test(a2) ? test(b2) ? then(a2, b2) : false : test(b2) ? false : void 0;
216
- }
217
- ancestors.add(a2);
218
- try {
219
- return check(Array.isArray, (a3, b3) => {
220
- if (a3.length !== b3.length) return false;
221
- for (let index = 0; index < a3.length; index++) if (!compare(a3[index], b3[index])) return false;
222
- return true;
223
- }) ?? 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) => {
224
- if (a3.byteLength !== b3.byteLength) return false;
225
- const viewA = new Uint8Array(a3);
226
- const viewB = new Uint8Array(b3);
227
- for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
228
- return true;
229
- }) ?? ((!strict || [a2, b2].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
230
- ...a2,
231
- ...b2
232
- }).every((key) => compare(a2[key], b2[key])));
233
- } finally {
234
- ancestors.delete(a2);
235
- }
236
- }
237
- 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));
238
202
  }
239
203
  var Time;
240
204
  (function(Time2) {
@@ -306,7 +270,7 @@ var Time;
306
270
  Time2.template = template;
307
271
  })(Time || (Time = {}));
308
272
 
309
- // 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
310
274
  var kSchema = /* @__PURE__ */ Symbol.for("schemastery");
311
275
  var kValidationError = /* @__PURE__ */ Symbol.for("ValidationError");
312
276
  globalThis.__schemastery_index__ ??= 0;
@@ -483,7 +447,6 @@ Schema.prototype.pattern = function pattern(regexp) {
483
447
  return schema;
484
448
  };
485
449
  Schema.prototype.simplify = function simplify(value) {
486
- if (isVolatile(value)) value = value.get();
487
450
  if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
488
451
  if (isNullable(value)) return value;
489
452
  if (this.type === "object" || this.type === "dict") {
@@ -541,49 +504,12 @@ for (const key of [
541
504
  };
542
505
  return schema;
543
506
  } });
544
- Schema.prototype.volatile = function volatile() {
545
- if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
546
- return this.extra("volatile", true);
547
- };
548
507
  var resolvers = {};
549
- var checkedVolatile = /* @__PURE__ */ Symbol("checked-volatile-schema");
550
- function validateVolatileSchema(schema, path13 = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
551
- const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
552
- if (states.has(blocked)) return;
553
- states.add(blocked);
554
- seen.set(schema, states);
555
- if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path: path13 });
556
- const nested = blocked || !!schema.meta?.volatile;
557
- if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path13, key], nested, seen);
558
- if (schema.sKey) validateVolatileSchema(schema.sKey, [...path13, "<key>"], true, seen);
559
- if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path13, "*"], true, seen);
560
- if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path13, String(index)], true, seen);
561
- }
562
508
  Schema.extend = function extend(type, resolve2) {
563
509
  resolvers[type] = resolve2;
564
510
  };
565
511
  Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
566
512
  if (!schema) return [data];
567
- if (!options[checkedVolatile]) {
568
- validateVolatileSchema(schema, options.path);
569
- options = {
570
- ...options,
571
- [checkedVolatile]: true
572
- };
573
- }
574
- if (schema.meta?.volatile) {
575
- const inner = Schema(schema);
576
- inner.meta = {
577
- ...schema.meta,
578
- volatile: false
579
- };
580
- const [value, adapted] = Schema.resolve(data, inner, options, strict);
581
- try {
582
- return [createVolatile(value), adapted];
583
- } catch (error) {
584
- throw new ValidationError(error instanceof Error ? error.message : String(error), options);
585
- }
586
- }
587
513
  if (options.ignore?.(data, schema)) return [data];
588
514
  if (isNullable(data) && schema.type !== "lazy") {
589
515
  if (schema.meta.required) throw new ValidationError(`missing required value`, options);
@@ -691,7 +617,6 @@ Schema.extend("lazy", (data, schema, options, strict) => {
691
617
  ...schema.meta,
692
618
  ...schema.inner.meta
693
619
  };
694
- validateVolatileSchema(schema.inner, options.path, true);
695
620
  }
696
621
  return Schema.resolve(data, schema.inner, options, strict);
697
622
  });
@@ -791,7 +716,7 @@ function property(data, key, schema, options) {
791
716
  } catch (e) {
792
717
  if (!options?.autofix) throw e;
793
718
  delete data[key];
794
- return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
719
+ return schema.meta.default;
795
720
  }
796
721
  }
797
722
  Schema.extend("array", (data, { inner, meta }, options) => {
@@ -993,7 +918,7 @@ function saveUiConfig(guard, settingsDir, patch) {
993
918
  // src/core/orchestrator.ts
994
919
  import { randomUUID as randomUUID2 } from "crypto";
995
920
  import fs10 from "fs";
996
- import path9 from "path";
921
+ import path10 from "path";
997
922
 
998
923
  // src/env/envpulse.ts
999
924
  import fs3 from "fs";
@@ -1121,7 +1046,7 @@ function collectPulse(guard, paths, rules, fgProcess = null, now = /* @__PURE__
1121
1046
  const idle = probeIdle(guard, paths);
1122
1047
  const windowClass = classifyProcess(fgProcess, rules);
1123
1048
  const t = timeContext(now);
1124
- const snapshot2 = {
1049
+ const snapshot = {
1125
1050
  takenAt: now.toISOString(),
1126
1051
  idleSeconds: idle,
1127
1052
  presence: presenceOf(idle, windowClass),
@@ -1132,24 +1057,24 @@ function collectPulse(guard, paths, rules, fgProcess = null, now = /* @__PURE__
1132
1057
  festival: t.festival
1133
1058
  };
1134
1059
  try {
1135
- 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");
1136
1061
  } catch {
1137
1062
  }
1138
- writePulseStream(paths, snapshot2);
1139
- return snapshot2;
1063
+ writePulseStream(paths, snapshot);
1064
+ return snapshot;
1140
1065
  }
1141
- function writePulseStream(paths, snapshot2) {
1066
+ function writePulseStream(paths, snapshot) {
1142
1067
  try {
1143
1068
  appendAuditLine(path3.join(paths.logsDir, "envpulse.jsonl"), {
1144
1069
  event: "pulse",
1145
- takenAt: snapshot2.takenAt,
1146
- idleSeconds: snapshot2.idleSeconds,
1147
- presence: snapshot2.presence,
1148
- windowClass: snapshot2.windowClass,
1149
- daypart: snapshot2.daypart,
1150
- weekday: snapshot2.weekday,
1151
- isWeekend: snapshot2.isWeekend,
1152
- 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
1153
1078
  });
1154
1079
  } catch {
1155
1080
  }
@@ -1490,7 +1415,7 @@ function assembleCandidates(seeds, opts = {}) {
1490
1415
  }
1491
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";
1492
1417
  function materialLines(materials) {
1493
- return materials.map((m) => m.text.trim());
1418
+ return materials.map((m) => `- [${m.id}] ${m.text.trim()}`);
1494
1419
  }
1495
1420
  function wantHonestOption(materials, threshold = 2) {
1496
1421
  return materials.filter((m) => m.used >= 1).length >= threshold;
@@ -1502,22 +1427,60 @@ function buildMaterialPrompt(materials, opts = {}) {
1502
1427
  }
1503
1428
  lines.push(PACKAGE_DECLARE);
1504
1429
  const items = materialLines(materials);
1505
- for (const it of items) lines.push("- " + it);
1430
+ for (const it of items) lines.push(it);
1506
1431
  if (wantHonestOption(materials, opts.threshold ?? 2)) {
1507
1432
  lines.push("(\u6216\u8005\u4E5F\u53EF\u4EE5\u8BF4\u4E00\u53E5\u771F\u5FC3\u8BDD,\u4E0D\u5E26\u7D20\u6750)");
1508
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);
1509
1438
  return lines.join("\n");
1510
1439
  }
1511
- function attributionIds(candidates, output, explicitIds = []) {
1512
- const used = new Set(explicitIds);
1513
- const b = output.trim();
1514
- for (const c of candidates) {
1515
- const a = c.text.trim();
1516
- if (a.length >= 8 && (b.includes(a.slice(0, Math.min(20, a.length))) || a.includes(b.slice(0, Math.min(20, b.length))))) {
1517
- used.add(c.id);
1518
- }
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
+ };
1519
1460
  }
1520
- return [...used];
1461
+ if (!spokeText) {
1462
+ return { spoken: "silent", seedIds: [], profileIds: [], source: "none" };
1463
+ }
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
+ };
1521
1484
  }
1522
1485
  function buildRuminationPrompt(input) {
1523
1486
  const max = input.max ?? 3;
@@ -1558,9 +1521,128 @@ function buildRuminationPrompt(input) {
1558
1521
  return lines.join("\n");
1559
1522
  }
1560
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
+
1561
1643
  // src/statusbar/store.ts
1562
1644
  import fs7 from "fs";
1563
- import path7 from "path";
1645
+ import path8 from "path";
1564
1646
  function deriveScene(input) {
1565
1647
  if (input.quietHours) return "quiet-hours";
1566
1648
  if (input.spokeThisBeat) return "just-spoke";
@@ -1575,7 +1657,7 @@ function clampNote(note) {
1575
1657
  return t.length <= 30 ? t : t.slice(0, 30);
1576
1658
  }
1577
1659
  function statusFilePath(dataDir) {
1578
- return path7.join(dataDir, "settings", "status.json");
1660
+ return path8.join(dataDir, "settings", "status.json");
1579
1661
  }
1580
1662
  function writeStatus(guard, paths, state) {
1581
1663
  atomicWriteJsonSync(guard.assert(statusFilePath(paths.dataDir)), state);
@@ -1750,7 +1832,13 @@ async function runConsolidation(guard, paths, policy, llm, now = Date.now()) {
1750
1832
  const report = applyOpsToDoc(guard, paths.dataDir, doc, profileOpsCast, schema, policy, now);
1751
1833
  const aged = runDeterministicAging(doc, policy, now);
1752
1834
  persistWithJournal(guard, paths.dataDir, doc, { runId, applied: report.applied, rejected: report.rejected });
1753
- const { addSeed: addSeed2, seedsFilePath: seedsFilePath2 } = await import("./pool-JQC5JWIZ.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
+ };
1754
1842
  let chatSeedsAdded = 0;
1755
1843
  for (const cs of chatSeeds) {
1756
1844
  try {
@@ -1759,7 +1847,7 @@ async function runConsolidation(guard, paths, policy, llm, now = Date.now()) {
1759
1847
  ...cs.topic ? { topic: cs.topic } : {},
1760
1848
  source: "chat",
1761
1849
  tag: "scene"
1762
- }, now);
1850
+ }, now, seedAudit);
1763
1851
  chatSeedsAdded += 1;
1764
1852
  } catch {
1765
1853
  }
@@ -1793,6 +1881,10 @@ function fmtEntry(prefix, content, opts) {
1793
1881
  const flag = opts.low ? "\uFF08\u4E45\u672A\u9A8C\u8BC1\uFF09" : "";
1794
1882
  return `${prefix}${content}${flag} [conf ${opts.confidence.toFixed(2)}]`;
1795
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
+ }
1796
1888
  function buildDigest(guard, paths, policy, input = {}) {
1797
1889
  const doc = loadProfile(guard, profileFilePath(paths.dataDir));
1798
1890
  const topN = input.topN ?? 8;
@@ -1810,8 +1902,7 @@ function buildDigest(guard, paths, policy, input = {}) {
1810
1902
  `[\u65F6\u95F4\u611F] ${rhythmLine}${input.windowClass ? ` | \u5F53\u524D\u7A97\u53E3\u7C7B\u522B: ${input.windowClass}` : ""}`,
1811
1903
  ...comm
1812
1904
  ];
1813
- const score = (e) => e.confidence * 0.7 + 1 / (1 + Math.max(0, Date.now() - Date.parse(e.updatedAt)) / 864e5) * 0.3;
1814
- 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) => {
1815
1906
  const prefix = e.partition === "projects" ? "- \u8FDB\u884C\u4E2D: " : "- \u5174\u8DA3: ";
1816
1907
  return fmtEntry(prefix, `${e.topic}/${e.subTopic}: ${e.content}`, { low: e.lowActivity, confidence: e.confidence });
1817
1908
  });
@@ -1839,11 +1930,11 @@ function buildDigest(guard, paths, policy, input = {}) {
1839
1930
 
1840
1931
  // src/rhythm/rhythm.ts
1841
1932
  import fs8 from "fs";
1842
- import path8 from "path";
1933
+ import path9 from "path";
1843
1934
  var DAY_MS = 864e5;
1844
1935
  var TAU_DAYS = 10;
1845
1936
  function rhythmFilePath(paths) {
1846
- return path8.join(paths.dataDir, "profile_rhythm.json");
1937
+ return path9.join(paths.dataDir, "profile_rhythm.json");
1847
1938
  }
1848
1939
  function loadRhythm(paths) {
1849
1940
  try {
@@ -1914,6 +2005,7 @@ function collectWeeklyFacts(guard, paths, policy, now = Date.now()) {
1914
2005
  let spoken = 0;
1915
2006
  let silent = 0;
1916
2007
  let observedItems = 0;
2008
+ let reportMissing = 0;
1917
2009
  const reasons = /* @__PURE__ */ new Map();
1918
2010
  try {
1919
2011
  for (const e of readAuditLines(paths.logsDir + "/heartbeat.jsonl")) {
@@ -1921,6 +2013,7 @@ function collectWeeklyFacts(guard, paths, policy, now = Date.now()) {
1921
2013
  if (!inWindow(ts, start, end)) continue;
1922
2014
  const ev = e.event;
1923
2015
  if (ev === "spoke") spoken += 1;
2016
+ else if (ev === "report_missing") reportMissing += 1;
1924
2017
  else if (ev === "silent") {
1925
2018
  silent += 1;
1926
2019
  const reason = String(e.reason ?? "unknown").slice(0, 60);
@@ -1981,6 +2074,20 @@ function collectWeeklyFacts(guard, paths, policy, now = Date.now()) {
1981
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));
1982
2075
  } catch {
1983
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
+ }
1984
2091
  let peakHours = [];
1985
2092
  try {
1986
2093
  peakHours = summarizeRhythm(paths).peakHours;
@@ -1996,6 +2103,7 @@ function collectWeeklyFacts(guard, paths, policy, now = Date.now()) {
1996
2103
  profileAdds,
1997
2104
  ledger,
1998
2105
  seeds,
2106
+ reports,
1999
2107
  peakHours
2000
2108
  };
2001
2109
  }
@@ -2013,8 +2121,8 @@ function sessionEvents(session) {
2013
2121
  if (!session) return [];
2014
2122
  if (typeof session.snapshotEvents === "function") {
2015
2123
  try {
2016
- const snapshot2 = session.snapshotEvents();
2017
- if (Array.isArray(snapshot2)) return snapshot2;
2124
+ const snapshot = session.snapshotEvents();
2125
+ if (Array.isArray(snapshot)) return snapshot;
2018
2126
  } catch {
2019
2127
  }
2020
2128
  }
@@ -2051,7 +2159,7 @@ function noteBeat(verdict, detail) {
2051
2159
  lastBeat = { at: (/* @__PURE__ */ new Date()).toISOString(), verdict, ...detail };
2052
2160
  }
2053
2161
  function stateFile(paths) {
2054
- return path9.join(paths.dataDir, "gate.json");
2162
+ return path10.join(paths.dataDir, "gate.json");
2055
2163
  }
2056
2164
  function readBeatState(guard, paths) {
2057
2165
  try {
@@ -2370,10 +2478,18 @@ function parseJsonBlock(raw) {
2370
2478
  }
2371
2479
  throw new Error(text.includes("{") ? "unparseable JSON object in model output" : "no JSON object in model output");
2372
2480
  }
2481
+ function seedAuditSink(paths) {
2482
+ return (entry) => {
2483
+ try {
2484
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", entry);
2485
+ } catch {
2486
+ }
2487
+ };
2488
+ }
2373
2489
  async function maintenancePhase(bc) {
2374
2490
  const { deps, now } = bc;
2375
2491
  const { guard, paths, policy } = deps;
2376
- gcPool(guard, seedsFilePath(paths.dataDir), policy, now);
2492
+ gcPool(guard, seedsFilePath(paths.dataDir), policy, now, seedAuditSink(paths));
2377
2493
  pruneAuditFile(paths.logsDir + "/envpulse.jsonl", policy.retention.envPulseHours * 36e5, now);
2378
2494
  pruneAuditFile(paths.logsDir + "/heartbeat.jsonl", policy.retention.decisionLogDays * 864e5, now);
2379
2495
  snapshotIfDue(guard, paths.dataDir, paths.logsDir + "/heartbeat.jsonl", "maintenance-threshold", now);
@@ -2412,7 +2528,7 @@ async function observeBoundSessions(bc) {
2412
2528
  const data = loadBindings2(guard, paths.settingsDir);
2413
2529
  const targets = observeTargets(data);
2414
2530
  if (targets.length === 0) return;
2415
- const cursorFile = path9.join(paths.dataDir, "cursors.json");
2531
+ const cursorFile = path10.join(paths.dataDir, "cursors.json");
2416
2532
  let cursors = {};
2417
2533
  try {
2418
2534
  cursors = JSON.parse(fs10.readFileSync(cursorFile, "utf8"));
@@ -2458,13 +2574,16 @@ async function weeklyPhase(bc) {
2458
2574
  const { deps, agent, now } = bc;
2459
2575
  const { guard, paths, policy } = deps;
2460
2576
  if (policy.weekly.enabled === false) return;
2461
- if (!weeklyDue(guard, paths.dataDir, now)) return;
2462
2577
  const audit = (entry) => {
2463
2578
  try {
2464
2579
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", entry);
2465
2580
  } catch {
2466
2581
  }
2467
2582
  };
2583
+ if (ensureWeeklyAnchor(guard, paths.dataDir, now)) {
2584
+ audit({ event: "weekly_anchored" });
2585
+ }
2586
+ if (!weeklyDue(guard, paths.dataDir, now)) return;
2468
2587
  if (!agent) {
2469
2588
  audit({ event: "weekly_skipped", reason: "agentless beat" });
2470
2589
  return;
@@ -2590,7 +2709,7 @@ async function runWanderTurn(bc, prompt, focus, opts) {
2590
2709
  tag: "news",
2591
2710
  source: "browse",
2592
2711
  confidence: 0.4
2593
- }, now);
2712
+ }, now, seedAuditSink(paths));
2594
2713
  registered += 1;
2595
2714
  }
2596
2715
  } catch (e) {
@@ -2600,6 +2719,24 @@ async function runWanderTurn(bc, prompt, focus, opts) {
2600
2719
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: opts.label, focus, registered });
2601
2720
  return true;
2602
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
+ }
2603
2740
  async function deliverPackage(bc, materials, opts) {
2604
2741
  const { deps, now } = bc;
2605
2742
  const { guard, paths, policy } = deps;
@@ -2630,11 +2767,14 @@ async function deliverPackage(bc, materials, opts) {
2630
2767
  }
2631
2768
  const voiceSessionId = voiceAgent.session?.id ?? null;
2632
2769
  try {
2770
+ const deliveryId = `d${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
2633
2771
  const phrasePrompt = buildMaterialPrompt(materials, {
2772
+ deliveryId,
2634
2773
  // spec ②: the "我在干嘛" line rides on every delivery when vision
2635
2774
  // produced one this beat; absent otherwise (never invented).
2636
2775
  ...typeof opts.doing === "string" && opts.doing.trim() ? { doing: opts.doing.trim().slice(0, 80) } : {}
2637
2776
  });
2777
+ const turnStart = Date.now();
2638
2778
  let spokenRaw;
2639
2779
  try {
2640
2780
  spokenRaw = await agentTurn(bc.deps, voiceAgent, phrasePrompt, "expression", EXPRESSION_IDLE_WAIT_MS);
@@ -2650,9 +2790,31 @@ async function deliverPackage(bc, materials, opts) {
2650
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));
2651
2791
  const cnLine = [...spokenLines].reverse().find((l) => /[\u4e00-\u9fff]/.test(l));
2652
2792
  const text = (cnLine ?? spokenLines[spokenLines.length - 1] ?? "").slice(0, 200);
2653
- if (!text || !/[\u4e00-\u9fff]/.test(text)) {
2654
- appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "spoke_failed", reason: "non-Chinese output discarded" });
2655
- 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
+ }
2656
2818
  return;
2657
2819
  }
2658
2820
  const confirm = confirmSend(guard, policy, paths, "topic", text, now);
@@ -2661,12 +2823,31 @@ async function deliverPackage(bc, materials, opts) {
2661
2823
  noteBeat("spoke_failed", { reason: confirm.reason });
2662
2824
  return;
2663
2825
  }
2664
- const usedIds = new Set(attributionIds(materials, text, opts.seedIds));
2665
- for (const id of usedIds) {
2666
- 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
+ }
2667
2840
  }
2668
2841
  sendNewMessageHint(paths);
2669
- 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
+ });
2670
2851
  if (voiceSessionId && voiceSessionId !== homeId) {
2671
2852
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "delivered", sessionId: voiceSessionId });
2672
2853
  }
@@ -2705,9 +2886,13 @@ async function expressionPhases(bc) {
2705
2886
  const offered = assembleCandidates(activeSeeds(loadPool(guard, seedsFilePath(paths.dataDir))));
2706
2887
  if (offered.length === 0) {
2707
2888
  if (policy.heartbeat.idleMode && bc.agent) {
2708
- const idleTopics = digest.topic.trim();
2709
- if (idleTopics) {
2710
- 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
+ }));
2711
2896
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", { event: "idle_fallback", topics: fallback.length });
2712
2897
  return deliverPackage(bc, fallback, {
2713
2898
  seedIds: [],
@@ -2940,20 +3125,20 @@ function startOrchestrator(deps) {
2940
3125
  import { spawn as spawn2 } from "child_process";
2941
3126
  import fs13 from "fs";
2942
3127
  import os from "os";
2943
- import path12 from "path";
3128
+ import path13 from "path";
2944
3129
 
2945
3130
  // src/browse/interests-edit.ts
2946
3131
  import fs11 from "fs";
2947
- import path10 from "path";
3132
+ import path11 from "path";
2948
3133
  var MAX_INTERESTS = 32;
2949
3134
  var MAX_INTEREST_LEN = 60;
2950
3135
  var MAX_WINDOWS = 6;
2951
3136
  var HHMM = /^([01]\d|2[0-3]):([0-5]\d)$/;
2952
3137
  function userInterestsPath(paths) {
2953
- return path10.join(paths.settingsDir, "interests.json");
3138
+ return path11.join(paths.settingsDir, "interests.json");
2954
3139
  }
2955
3140
  function factoryInterestsPath(paths) {
2956
- return path10.join(paths.configDir, "interests.json");
3141
+ return path11.join(paths.configDir, "interests.json");
2957
3142
  }
2958
3143
  function readDoc(file) {
2959
3144
  try {
@@ -2975,7 +3160,7 @@ function ensureUserLayer(guard, paths) {
2975
3160
  return doc;
2976
3161
  }
2977
3162
  function saveDoc(guard, file, doc) {
2978
- fs11.mkdirSync(path10.dirname(file), { recursive: true });
3163
+ fs11.mkdirSync(path11.dirname(file), { recursive: true });
2979
3164
  fs11.writeFileSync(guard.assert(file), JSON.stringify(doc, null, 2), "utf8");
2980
3165
  }
2981
3166
  function normalizeInterest(text) {
@@ -3043,7 +3228,7 @@ function setWanderWindows(guard, paths, rawWindows) {
3043
3228
 
3044
3229
  // src/statusbar/time-inject.ts
3045
3230
  import fs12 from "fs";
3046
- import path11 from "path";
3231
+ import path12 from "path";
3047
3232
  function shouldInjectTime(input) {
3048
3233
  if (input.step !== 1) return false;
3049
3234
  if (!input.originIsInboxSplice) return false;
@@ -3105,7 +3290,7 @@ function lastMessageTime(session) {
3105
3290
  return void 0;
3106
3291
  }
3107
3292
  function timeInjectStatePath(dataDir) {
3108
- return path11.join(dataDir, "time-inject-state.json");
3293
+ return path12.join(dataDir, "time-inject-state.json");
3109
3294
  }
3110
3295
  function loadTimeInjectState(guard, dataDir) {
3111
3296
  try {
@@ -3174,7 +3359,7 @@ var cachedVersion = null;
3174
3359
  function pluginVersion(paths) {
3175
3360
  if (cachedVersion) return cachedVersion;
3176
3361
  try {
3177
- const pkg = JSON.parse(fs13.readFileSync(path12.join(paths.packageRoot, "package.json"), "utf8"));
3362
+ const pkg = JSON.parse(fs13.readFileSync(path13.join(paths.packageRoot, "package.json"), "utf8"));
3178
3363
  cachedVersion = typeof pkg.version === "string" ? pkg.version : "unknown";
3179
3364
  } catch {
3180
3365
  cachedVersion = "unknown";
@@ -3183,12 +3368,20 @@ function pluginVersion(paths) {
3183
3368
  }
3184
3369
  function homeSessionId(paths, guard) {
3185
3370
  try {
3186
- const raw = loadEncryptedText(guard, path12.join(paths.dataDir, "gate.json"));
3371
+ const raw = loadEncryptedText(guard, path13.join(paths.dataDir, "gate.json"));
3187
3372
  return JSON.parse(raw ?? "{}").sessionId ?? null;
3188
3373
  } catch {
3189
3374
  return null;
3190
3375
  }
3191
3376
  }
3377
+ function seedAuditSink2(paths) {
3378
+ return (entry) => {
3379
+ try {
3380
+ appendAuditLine(paths.logsDir + "/heartbeat.jsonl", entry);
3381
+ } catch {
3382
+ }
3383
+ };
3384
+ }
3192
3385
  function auditInterests(paths, action, result, detail) {
3193
3386
  if (!result.ok) return;
3194
3387
  appendAuditLine(paths.logsDir + "/heartbeat.jsonl", {
@@ -3205,13 +3398,13 @@ function loadSessionTitles() {
3205
3398
  if (row && typeof row.val === "string" && row.val && !titles[id]) titles[id] = row.val;
3206
3399
  };
3207
3400
  try {
3208
- const dir = path12.join(os.homedir(), ".dsh", "storages", "session_projcache", "sessions");
3401
+ const dir = path13.join(os.homedir(), ".dsh", "storages", "session_projcache", "sessions");
3209
3402
  for (const file of fs13.readdirSync(dir)) {
3210
3403
  if (!file.endsWith(".json")) continue;
3211
3404
  const id = file.slice(0, -".json".length);
3212
3405
  if (!id.startsWith("session-")) continue;
3213
3406
  try {
3214
- const record = JSON.parse(fs13.readFileSync(path12.join(dir, file), "utf8"));
3407
+ const record = JSON.parse(fs13.readFileSync(path13.join(dir, file), "utf8"));
3215
3408
  take(id, record.record);
3216
3409
  } catch {
3217
3410
  }
@@ -3219,7 +3412,7 @@ function loadSessionTitles() {
3219
3412
  } catch {
3220
3413
  }
3221
3414
  try {
3222
- const raw = JSON.parse(fs13.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"));
3223
3416
  const walk = (node) => {
3224
3417
  if (!node || typeof node !== "object") return;
3225
3418
  for (const [key, value] of Object.entries(node)) {
@@ -3237,7 +3430,7 @@ function loadSessionTitles() {
3237
3430
  }
3238
3431
  function loadVisibleSessionIds(registryFile) {
3239
3432
  try {
3240
- const file = registryFile ?? path12.join(os.homedir(), ".dsh", "storages", "workspace.json");
3433
+ const file = registryFile ?? path13.join(os.homedir(), ".dsh", "storages", "workspace.json");
3241
3434
  const j = JSON.parse(fs13.readFileSync(file, "utf8"));
3242
3435
  const archived = new Set(
3243
3436
  (Array.isArray(j.global?.archivedSessionIds) ? j.global.archivedSessionIds : []).map((s) => String(s))
@@ -3309,7 +3502,7 @@ function installHeartbeatRpc(ctx, deps) {
3309
3502
  });
3310
3503
  }
3311
3504
  case "sessions.list": {
3312
- const root = path12.join(os.homedir(), ".dsh", "sessions");
3505
+ const root = path13.join(os.homedir(), ".dsh", "sessions");
3313
3506
  const bindings = loadBindings(guard, paths.settingsDir).bindings;
3314
3507
  const home = homeSessionId(paths, guard);
3315
3508
  const titles = loadSessionTitles();
@@ -3317,7 +3510,7 @@ function installHeartbeatRpc(ctx, deps) {
3317
3510
  const out = [];
3318
3511
  if (fs13.existsSync(root)) {
3319
3512
  for (const slug of fs13.readdirSync(root)) {
3320
- for (const id of fs13.readdirSync(path12.join(root, slug))) {
3513
+ for (const id of fs13.readdirSync(path13.join(root, slug))) {
3321
3514
  if (registryFound && id !== home && !visible.has(id)) continue;
3322
3515
  const binding = bindings.find((b) => b.sessionId === id);
3323
3516
  out.push({
@@ -3353,7 +3546,7 @@ function installHeartbeatRpc(ctx, deps) {
3353
3546
  let homeReset = false;
3354
3547
  if (id === homeSessionId(paths, guard)) {
3355
3548
  try {
3356
- fs13.rmSync(guard.assert(path12.join(paths.dataDir, "gate.json")), { force: true });
3549
+ fs13.rmSync(guard.assert(path13.join(paths.dataDir, "gate.json")), { force: true });
3357
3550
  homeReset = true;
3358
3551
  } catch {
3359
3552
  }
@@ -3369,15 +3562,15 @@ function installHeartbeatRpc(ctx, deps) {
3369
3562
  });
3370
3563
  }
3371
3564
  case "seeds.archive": {
3372
- 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));
3373
3566
  return s ? ok(s) : err("not-found", "active seed not found");
3374
3567
  }
3375
3568
  case "seeds.restore": {
3376
- 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));
3377
3570
  return r.ok ? ok(r.seed) : err("restore-failed", r.reason);
3378
3571
  }
3379
3572
  case "seeds.delete": {
3380
- 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)) });
3381
3574
  }
3382
3575
  // ── 兴趣范围 / 浏览时段(v1.4.0;首编继承出厂,见 interests-edit.ts)──
3383
3576
  case "interests.list":
@@ -3435,7 +3628,7 @@ function installHeartbeatRpc(ctx, deps) {
3435
3628
  lines.push(`- [${e.topic}/${e.subTopic}] ${e.content} (conf ${e.confidence.toFixed(2)}, ${e.temporal})`);
3436
3629
  }
3437
3630
  }
3438
- const out = path12.join(paths.exportsDir, `profile-export-${Date.now()}.md`);
3631
+ const out = path13.join(paths.exportsDir, `profile-export-${Date.now()}.md`);
3439
3632
  writeText(guard, out, lines.join("\n") + "\n");
3440
3633
  return ok({ path: out });
3441
3634
  }
@@ -3456,10 +3649,10 @@ function installHeartbeatRpc(ctx, deps) {
3456
3649
  case "migrate.export": {
3457
3650
  const passphrase = String(p.passphrase ?? "");
3458
3651
  if (!passphrase) return err("bad-request", "\u9700\u8981\u8BBE\u7F6E\u53E3\u4EE4");
3459
- const { collectMigrationEntries, encryptContainer } = await import("./migrate-43PUK25G.js");
3652
+ const { collectMigrationEntries, encryptContainer } = await import("./migrate-N6NXMZ3G.js");
3460
3653
  const { entries } = collectMigrationEntries(guard, paths);
3461
3654
  if (entries.length === 0) return err("bad-request", "\u6CA1\u6709\u53EF\u6253\u5305\u7684\u8BB0\u5FC6\u6587\u4EF6\uFF08data/ \u662F\u7A7A\u7684\uFF09");
3462
- const out = path12.join(paths.exportsDir, `heartbeat-memory-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.hbmig`);
3655
+ const out = path13.join(paths.exportsDir, `heartbeat-memory-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.hbmig`);
3463
3656
  writeText(guard, out, encryptContainer(entries, passphrase));
3464
3657
  appendAuditLine(guard.assert(paths.logsDir + "/heartbeat.jsonl"), { event: "migrate_export", files: entries.length });
3465
3658
  return ok({ path: out, count: entries.length });
@@ -3468,7 +3661,7 @@ function installHeartbeatRpc(ctx, deps) {
3468
3661
  const passphrase = String(p.passphrase ?? "");
3469
3662
  const file = String(p.file ?? "");
3470
3663
  if (!passphrase || !file) return err("bad-request", "\u9700\u8981\u5BB9\u5668\u8DEF\u5F84\u4E0E\u53E3\u4EE4");
3471
- const { decryptContainer, applyMigrationEntries } = await import("./migrate-43PUK25G.js");
3664
+ const { decryptContainer, applyMigrationEntries } = await import("./migrate-N6NXMZ3G.js");
3472
3665
  let text;
3473
3666
  try {
3474
3667
  text = fs13.readFileSync(file, "utf8");
@@ -3721,6 +3914,8 @@ var Config = Schema.object({
3721
3914
  tokenSaver: Schema.boolean().default(false),
3722
3915
  /** v1.8.0 账本工具:向所有日常会话 agent 注册共享账本工具(默认开)。 */
3723
3916
  ledgerTool: Schema.boolean().default(true),
3917
+ /** v1.9.0 素材报账工具:投递后由陪伴 agent 主动报账(默认开)。 */
3918
+ seedReportTool: Schema.boolean().default(true),
3724
3919
  /** v1.8.0 引擎室追加工具:逗号分隔的全局工具名,存在才加入白名单(bili 压缩工具自动探测,无需手填)。 */
3725
3920
  extraTools: Schema.string().default("")
3726
3921
  });
@@ -3895,6 +4090,24 @@ function apply(ctx, config = {}) {
3895
4090
  }
3896
4091
  });
3897
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
+ }
3898
4111
  startOrchestrator(deps);
3899
4112
  const statusReader = new StatusReader();
3900
4113
  registerStatusbarSection(ctx, guard, paths, {