@kanadego/dsh-heartbeat 1.7.4 → 1.8.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.
@@ -3,13 +3,18 @@ import {
3
3
  loadPool,
4
4
  normalizeCategory,
5
5
  seedsFilePath
6
- } from "./chunk-SVP2NDRF.js";
6
+ } from "./chunk-MXJPX3FM.js";
7
7
  import {
8
+ atomicWriteJsonSync
9
+ } from "./chunk-VIZNIQLK.js";
10
+ import {
11
+ loadEncryptedText,
8
12
  loadJson,
9
13
  readText,
14
+ saveEncryptedText,
10
15
  saveJson,
11
16
  writeText
12
- } from "./chunk-LLD7LUNN.js";
17
+ } from "./chunk-IFTFDHZX.js";
13
18
 
14
19
  // src/core/path-guard.ts
15
20
  import fs from "fs";
@@ -71,80 +76,6 @@ function createPathGuard(workspaceDir) {
71
76
  return guard;
72
77
  }
73
78
 
74
- // src/core/audit-log.ts
75
- import fs3 from "fs";
76
- import path3 from "path";
77
-
78
- // src/core/atomic-fs.ts
79
- import fs2 from "fs";
80
- import path2 from "path";
81
- import { randomUUID, randomFillSync } from "crypto";
82
- function tmpSibling(target, tag = "w") {
83
- return path2.join(
84
- path2.dirname(target),
85
- `.${path2.basename(target)}.${tag}-${randomUUID().slice(0, 8)}.tmp`
86
- );
87
- }
88
- function atomicWriteFileSync(target, data) {
89
- const tmp = tmpSibling(target);
90
- try {
91
- fs2.writeFileSync(tmp, data);
92
- fs2.renameSync(tmp, target);
93
- } finally {
94
- fs2.rmSync(tmp, { force: true });
95
- }
96
- }
97
- function atomicWriteJsonSync(target, value) {
98
- atomicWriteFileSync(target, JSON.stringify(value, null, 2));
99
- }
100
- function shredFileSync(target, passes = 3) {
101
- const stat = fs2.statSync(target);
102
- if (!stat.isFile()) throw new Error(`shred: not a file: ${target}`);
103
- const buf = Buffer.alloc(Math.max(stat.size, 1));
104
- for (let i = 0; i < passes; i++) {
105
- randomFillSync(buf);
106
- fs2.writeFileSync(target, buf);
107
- }
108
- fs2.rmSync(target, { force: true });
109
- }
110
-
111
- // src/core/audit-log.ts
112
- function appendAuditLine(file, event) {
113
- fs3.mkdirSync(path3.dirname(file), { recursive: true });
114
- const line = JSON.stringify({ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(), ...event });
115
- fs3.appendFileSync(file, line + "\n", "utf8");
116
- }
117
- function readAuditLines(file) {
118
- if (!fs3.existsSync(file)) return [];
119
- const out = [];
120
- const raw = fs3.readFileSync(file, "utf8");
121
- for (const line of raw.split("\n")) {
122
- const trimmed = line.trim();
123
- if (!trimmed) continue;
124
- try {
125
- out.push(JSON.parse(trimmed));
126
- } catch {
127
- out.push({ ts: "", corrupt: true, raw: trimmed.slice(0, 200) });
128
- }
129
- }
130
- return out;
131
- }
132
- function pruneAuditFile(file, maxAgeMs, now = Date.now()) {
133
- if (!fs3.existsSync(file)) return 0;
134
- const lines = readAuditLines(file);
135
- const kept = lines.filter((e) => {
136
- const ev = e;
137
- const ts = Date.parse(ev.ts ?? "");
138
- if (!Number.isFinite(ts)) return true;
139
- return now - ts <= maxAgeMs;
140
- });
141
- const removed = lines.length - kept.length;
142
- if (removed === 0) return 0;
143
- const body = kept.map((e) => JSON.stringify(e)).join("\n");
144
- atomicWriteFileSync(file, body ? body + "\n" : "");
145
- return removed;
146
- }
147
-
148
79
  // src/config/schema.ts
149
80
  var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
150
81
  function isPlainObject(v) {
@@ -162,6 +93,7 @@ function assertPolicy(input) {
162
93
  fail("heartbeat.intervalMin must be a number in [1, 1440]");
163
94
  }
164
95
  if (typeof hb.idleMode !== "boolean") fail("heartbeat.idleMode must be boolean");
96
+ if (typeof hb.archiveRotatedHome !== "boolean") fail("heartbeat.archiveRotatedHome must be boolean");
165
97
  const g = p.gate;
166
98
  if (!isPlainObject(g)) fail("gate missing");
167
99
  if (typeof g.maxDailySend !== "number" || g.maxDailySend < 0) fail("gate.maxDailySend must be >= 0");
@@ -207,6 +139,12 @@ function assertPolicy(input) {
207
139
  if (typeof pr.volatileDays !== "number" || pr.volatileDays < 1) fail("profile.volatileDays must be >= 1");
208
140
  if (typeof pr.stableLowActivityDays !== "number" || pr.stableLowActivityDays < 1) fail("profile.stableLowActivityDays must be >= 1");
209
141
  if (typeof pr.psyEnabled !== "boolean") fail("profile.psyEnabled must be boolean");
142
+ const ob = p.observe;
143
+ if (!isPlainObject(ob)) fail("observe missing");
144
+ if (typeof ob.maxChars !== "number" || ob.maxChars < 20 || ob.maxChars > 2e3) fail("observe.maxChars must be in [20, 2000]");
145
+ if (typeof ob.perBeat !== "number" || ob.perBeat < 1 || ob.perBeat > 100) fail("observe.perBeat must be in [1, 100]");
146
+ if (!isPlainObject(p.weekly)) fail("weekly missing");
147
+ if (typeof p.weekly.enabled !== "boolean") fail("weekly.enabled must be boolean");
210
148
  const r = p.retention;
211
149
  if (!isPlainObject(r)) fail("retention missing");
212
150
  if (typeof r.envPulseHours !== "number" || typeof r.decisionLogDays !== "number") fail("retention fields missing");
@@ -223,23 +161,23 @@ function deepMerge(base, override) {
223
161
  }
224
162
 
225
163
  // src/config/load.ts
226
- import fs4 from "fs";
227
- import path4 from "path";
164
+ import fs2 from "fs";
165
+ import path2 from "path";
228
166
  var USER_POLICY_FILE = "policy.json";
229
167
  function loadPolicy(guard, configDir, settingsDir) {
230
- const factoryPath = path4.join(configDir, "policy.json");
168
+ const factoryPath = path2.join(configDir, "policy.json");
231
169
  let factoryRaw;
232
170
  try {
233
- factoryRaw = JSON.parse(fs4.readFileSync(factoryPath, "utf8"));
171
+ factoryRaw = JSON.parse(fs2.readFileSync(factoryPath, "utf8"));
234
172
  } catch (e) {
235
173
  throw new Error(`factory policy unreadable at ${factoryPath}: ${String(e)}`);
236
174
  }
237
175
  assertPolicy(factoryRaw);
238
- const userPath = guard.assert(path4.join(settingsDir, USER_POLICY_FILE));
176
+ const userPath = guard.assert(path2.join(settingsDir, USER_POLICY_FILE));
239
177
  let merged = factoryRaw;
240
- if (fs4.existsSync(userPath)) {
178
+ if (fs2.existsSync(userPath)) {
241
179
  try {
242
- const userRaw = JSON.parse(fs4.readFileSync(userPath, "utf8"));
180
+ const userRaw = JSON.parse(fs2.readFileSync(userPath, "utf8"));
243
181
  merged = deepMerge(factoryRaw, userRaw);
244
182
  } catch (e) {
245
183
  throw new Error(`user policy layer unparseable at ${userPath}: ${String(e)}`);
@@ -249,26 +187,26 @@ function loadPolicy(guard, configDir, settingsDir) {
249
187
  return merged;
250
188
  }
251
189
  function updateUserPolicy(guard, settingsDir, patch) {
252
- const userPath = guard.assert(path4.join(settingsDir, USER_POLICY_FILE));
190
+ const userPath = guard.assert(path2.join(settingsDir, USER_POLICY_FILE));
253
191
  let user = {};
254
192
  try {
255
- user = JSON.parse(fs4.readFileSync(userPath, "utf8"));
193
+ user = JSON.parse(fs2.readFileSync(userPath, "utf8"));
256
194
  if (!user || typeof user !== "object" || Array.isArray(user)) user = {};
257
195
  } catch {
258
196
  user = {};
259
197
  }
260
198
  const merged = deepMerge(user, patch);
261
- fs4.mkdirSync(path4.dirname(userPath), { recursive: true });
262
- fs4.writeFileSync(userPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
199
+ fs2.mkdirSync(path2.dirname(userPath), { recursive: true });
200
+ fs2.writeFileSync(userPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
263
201
  return merged;
264
202
  }
265
203
 
266
204
  // src/ledger/ledger.ts
267
- import path5 from "path";
268
- import { randomUUID as randomUUID2 } from "crypto";
205
+ import path3 from "path";
206
+ import { randomUUID } from "crypto";
269
207
  var DAY_MS = 864e5;
270
208
  function ledgerFilePath(dataDir) {
271
- return path5.join(dataDir, "ledger.md");
209
+ return path3.join(dataDir, "ledger.md");
272
210
  }
273
211
  var LINE_RE = /^- \[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})\]\[(open|done)\]\[#([0-9a-f]{6})\] (.*)$/;
274
212
  function renderEntry(e) {
@@ -294,7 +232,7 @@ function appendEntry(guard, file, text, now = Date.now()) {
294
232
  const d = new Date(now);
295
233
  const pad = (n) => String(n).padStart(2, "0");
296
234
  const entry = {
297
- id: randomUUID2().slice(0, 6),
235
+ id: randomUUID().slice(0, 6),
298
236
  date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,
299
237
  time: `${pad(d.getHours())}:${pad(d.getMinutes())}`,
300
238
  status: "open",
@@ -311,9 +249,10 @@ function markDone(guard, file, key, now = Date.now()) {
311
249
  if (!target) return null;
312
250
  const d = new Date(now);
313
251
  const pad = (n) => String(n).padStart(2, "0");
252
+ const doneDate = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
314
253
  const out = rawLines.map((line) => {
315
254
  if (line.includes(`#${target.id}] `)) {
316
- return `- [${target.date} ${pad(d.getHours())}:${pad(d.getMinutes())}][done][#${target.id}] ${target.text}`;
255
+ return `- [${doneDate} ${pad(d.getHours())}:${pad(d.getMinutes())}][done][#${target.id}] ${target.text}`;
317
256
  }
318
257
  return line;
319
258
  });
@@ -330,32 +269,32 @@ function pendingOlderThan(guard, file, days, now = Date.now()) {
330
269
  }
331
270
 
332
271
  // src/browse/browse.ts
333
- import fs5 from "fs";
334
- import path6 from "path";
272
+ import fs3 from "fs";
273
+ import path4 from "path";
335
274
  var WATCH_THROTTLE_MS = 6 * 36e5;
336
275
  var UA = { "User-Agent": "dsh-heartbeat/2.0 (+local; personal companion)" };
337
276
  function emptyBrowseState() {
338
277
  return { targets: {}, last_check_at: 0, wander: { focusHistory: {}, focusCount: {}, last_wander_at: 0, refillCount: {} } };
339
278
  }
340
279
  function browseStatePath(paths) {
341
- return path6.join(paths.dataDir, "browse.json");
280
+ return path4.join(paths.dataDir, "browse.json");
342
281
  }
343
282
  function readJsonFile(file, fallback) {
344
283
  try {
345
- return JSON.parse(fs5.readFileSync(file, "utf8"));
284
+ return JSON.parse(fs3.readFileSync(file, "utf8"));
346
285
  } catch {
347
286
  return fallback;
348
287
  }
349
288
  }
350
289
  function loadInterests(paths) {
351
- const userPath = path6.join(paths.settingsDir, "interests.json");
352
- if (fs5.existsSync(userPath)) return readJsonFile(userPath, { interests: [], _schedule: {} });
353
- return readJsonFile(path6.join(paths.configDir, "interests.json"), { interests: [], _schedule: {} });
290
+ const userPath = path4.join(paths.settingsDir, "interests.json");
291
+ if (fs3.existsSync(userPath)) return readJsonFile(userPath, { interests: [], _schedule: {} });
292
+ return readJsonFile(path4.join(paths.configDir, "interests.json"), { interests: [], _schedule: {} });
354
293
  }
355
294
  function loadWatchlist(paths) {
356
- const userPath = path6.join(paths.settingsDir, "watchlist.json");
357
- if (fs5.existsSync(userPath)) return readJsonFile(userPath, { targets: [] });
358
- return readJsonFile(path6.join(paths.configDir, "watchlist.json"), { targets: [] });
295
+ const userPath = path4.join(paths.settingsDir, "watchlist.json");
296
+ if (fs3.existsSync(userPath)) return readJsonFile(userPath, { targets: [] });
297
+ return readJsonFile(path4.join(paths.configDir, "watchlist.json"), { targets: [] });
359
298
  }
360
299
  function loadState(guard, paths) {
361
300
  return loadJson(guard, browseStatePath(paths)) ?? emptyBrowseState();
@@ -493,376 +432,139 @@ function browseStatus(guard, paths) {
493
432
  return loadState(guard, paths);
494
433
  }
495
434
 
496
- // src/profile/store.ts
497
- import path8 from "path";
498
- import { randomUUID as randomUUID3 } from "crypto";
499
- import fs7 from "fs";
500
-
501
- // src/profile/types.ts
502
- var PARTITIONS = ["interest", "projects", "comm", "psy"];
503
- var CONFIDENCE_CAP = {
504
- chat: 0.6,
505
- screen: 0.4,
506
- browse: 0.4,
507
- hand: 1,
508
- ledger: 0.6
509
- };
510
- function emptyProfile() {
511
- return {
512
- version: 1,
513
- partitions: { interest: { entries: [] }, projects: { entries: [] }, comm: { entries: [] }, psy: { entries: [] } }
514
- };
435
+ // src/notify/notify.ts
436
+ import { spawnSync } from "child_process";
437
+ import path5 from "path";
438
+ function runNotify(paths, args) {
439
+ const r = spawnSync(
440
+ "powershell.exe",
441
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path5.join(paths.assetsDir, "notify.ps1"), ...args],
442
+ { timeout: 2e4, encoding: "utf8" }
443
+ );
444
+ return { status: r.status ?? -1, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
515
445
  }
516
-
517
- // src/profile/schema.ts
518
- import fs6 from "fs";
519
- import path7 from "path";
520
- function loadProfileSchema(paths) {
521
- const userPath = path7.join(paths.settingsDir, "profile-schema.json");
522
- const file = fs6.existsSync(userPath) ? userPath : path7.join(paths.configDir, "profile-schema.json");
523
- try {
524
- const raw = JSON.parse(fs6.readFileSync(file, "utf8"));
525
- if (!raw.partitions) throw new Error("partitions missing");
526
- return raw;
527
- } catch (e) {
528
- throw new Error(`profile-schema unreadable at ${file}: ${String(e)}`);
529
- }
446
+ function ensureRegistered(paths) {
447
+ const check = runNotify(paths, ["-Check"]);
448
+ if (/REGISTERED: yes/.test(check.out)) return true;
449
+ const reg = runNotify(paths, ["-RegisterOnly"]);
450
+ return reg.status === 0;
530
451
  }
531
- function checkAddAgainstSchema(schema, partition, topic, subTopic, nominated) {
532
- const p = schema.partitions[partition];
533
- if (!p) return { ok: false, reason: `partition not in schema: ${partition}`, temporal: "stable" };
534
- const t = p.topics[topic];
535
- if (!t) return { ok: false, reason: `topic not in schema: ${partition}/${topic}`, temporal: "stable" };
536
- const st = t.subtopics[subTopic];
537
- if (!st) return { ok: false, reason: `sub_topic not in schema: ${partition}/${topic}/${subTopic}`, temporal: "stable" };
538
- const allowed = st.allowed && st.allowed.length > 0 ? st.allowed : ["stable"];
539
- const def = st.default && allowed.includes(st.default) ? st.default : allowed[0];
540
- if (!nominated) return { ok: true, temporal: def };
541
- if (!allowed.includes(nominated)) {
542
- return {
543
- ok: false,
544
- reason: `temporal "${nominated}" not allowed for ${partition}/${topic}/${subTopic} (allowed: ${allowed.join("|")})`,
545
- temporal: def
546
- };
547
- }
548
- return { ok: true, temporal: nominated };
452
+ function sendNewMessageHint(paths) {
453
+ const r = runNotify(paths, ["-Title", "Heartbeat", "-Message", "\u6709\u65B0\u6D88\u606F"]);
454
+ return r.status === 0 && /TOAST_SENT/.test(r.out);
549
455
  }
550
-
551
- // src/profile/store.ts
552
- var DAY_MS2 = 864e5;
553
- function profileFilePath(dataDir) {
554
- return path8.join(dataDir, "profile.json");
456
+ function sendWeeklyReadyHint(paths) {
457
+ const r = runNotify(paths, ["-Title", "\u5FC3\u8DF3\u5468\u62A5", "-Message", "\u672C\u671F\u5468\u62A5\u5DF2\u751F\u6210"]);
458
+ return r.status === 0 && /TOAST_SENT/.test(r.out);
555
459
  }
556
- function journalFilePath(dataDir) {
557
- return path8.join(dataDir, "profile_journal.jsonl");
460
+
461
+ // src/weekly/report.ts
462
+ import fs4 from "fs";
463
+ import path6 from "path";
464
+ var WEEKLY_INTERVAL_MS = 7 * 864e5;
465
+ function weeklyDirPath(dataDir) {
466
+ return path6.join(dataDir, "weekly");
558
467
  }
559
- function loadProfile(guard, file) {
560
- const doc = loadJson(guard, file);
561
- if (!doc || !doc.partitions) return emptyProfile();
562
- for (const p of PARTITIONS) {
563
- if (!doc.partitions[p]) doc.partitions[p] = { entries: [] };
564
- }
565
- return doc;
468
+ function stateFilePath(dataDir) {
469
+ return path6.join(weeklyDirPath(dataDir), "state.json");
566
470
  }
567
- function parseIso(v) {
568
- const t = Date.parse(v);
569
- return Number.isFinite(t) ? t : 0;
471
+ function reportFileName(endIso) {
472
+ return `report-${endIso.slice(0, 10)}.json`;
570
473
  }
571
- function refExists(guard, dataDir, ref) {
572
- const base = ref.split("#")[0] ?? "";
573
- if (!base) return false;
574
- const target = path8.join(dataDir, base);
474
+ function lastWeeklyGeneratedAt(guard, dataDir) {
575
475
  try {
576
- return fs7.existsSync(guard.assert(target));
476
+ const raw = JSON.parse(fs4.readFileSync(guard.assert(stateFilePath(dataDir)), "utf8"));
477
+ return Number(raw.lastGeneratedAt) || 0;
577
478
  } catch {
578
- return false;
479
+ return 0;
579
480
  }
580
481
  }
581
- function capForKinds(kinds) {
582
- if (kinds.length === 0) return 0.4;
583
- return Math.min(...kinds.map((k) => CONFIDENCE_CAP[k] ?? 0.4));
482
+ function weeklyDue(guard, dataDir, now = Date.now()) {
483
+ const last = lastWeeklyGeneratedAt(guard, dataDir);
484
+ return now - last >= WEEKLY_INTERVAL_MS;
584
485
  }
585
- function findActive(doc, id) {
586
- for (const p of PARTITIONS) {
587
- const hit = doc.partitions[p].entries.find((e) => e.id === id && e.validTo === null);
588
- if (hit) return hit;
589
- }
590
- return void 0;
486
+ function saveWeeklyReport(guard, dataDir, report) {
487
+ const dir = weeklyDirPath(dataDir);
488
+ fs4.mkdirSync(dir, { recursive: true });
489
+ const file = path6.join(dir, reportFileName(report.end));
490
+ saveEncryptedText(guard, file, JSON.stringify(report, null, 1));
491
+ atomicWriteJsonSync(stateFilePath(dataDir), { lastGeneratedAt: Date.parse(report.generatedAt) });
492
+ return file;
591
493
  }
592
- function applyOpsToDoc(guard, dataDir, doc, ops, schema, policy, now) {
593
- const applied = [];
594
- const rejected = [];
595
- const nowIso = new Date(now).toISOString();
596
- for (const op of ops) {
597
- if (op.op === "NOOP") {
598
- applied.push(op);
599
- continue;
600
- }
601
- if (op.op === "ADD") {
602
- if (op.partition === "psy" && !policy.profile.psyEnabled) {
603
- rejected.push({ op, reason: "psy partition is disabled" });
604
- continue;
605
- }
606
- const check = checkAddAgainstSchema(schema, op.partition, op.topic, op.subTopic, op.temporal);
607
- if (!check.ok) {
608
- rejected.push({ op, reason: check.reason });
609
- continue;
610
- }
611
- if (!op.evidence || op.evidence.length === 0) {
612
- rejected.push({ op, reason: "ADD without evidence (no provenance, axiom 1)" });
613
- continue;
614
- }
615
- const badRef = op.evidence.find((e) => !refExists(guard, dataDir, e.ref));
616
- if (badRef) {
617
- rejected.push({ op, reason: `evidence ref does not resolve: ${badRef.ref}` });
618
- continue;
619
- }
620
- const cap = capForKinds(op.evidence.map((e) => e.kind));
621
- const active = doc.partitions[op.partition].entries.filter((e) => e.validTo === null);
622
- if (active.length >= policy.profile.partitionCap) {
623
- rejected.push({ op, reason: `partition ${op.partition} at cap (${policy.profile.partitionCap}); converge first` });
624
- continue;
625
- }
626
- dbSeq += 1;
627
- const entry = {
628
- id: `p${dbSeq.toString(36)}${randomUUID3().slice(0, 4)}`,
629
- partition: op.partition,
630
- topic: op.topic,
631
- subTopic: op.subTopic,
632
- content: op.content.trim(),
633
- confidence: Math.min(op.confidence ?? cap, cap),
634
- temporal: check.temporal,
635
- validFrom: nowIso,
636
- validTo: null,
637
- supersededBy: null,
638
- evidence: op.evidence,
639
- createdAt: nowIso,
640
- updatedAt: nowIso,
641
- updateCount: 0
642
- };
643
- op.assignedId = entry.id;
644
- doc.partitions[op.partition].entries.push(entry);
645
- applied.push(op);
646
- continue;
647
- }
648
- if (op.op === "UPDATE") {
649
- const entry = findActive(doc, op.id);
650
- if (!entry) {
651
- rejected.push({ op, reason: `unknown or inactive entry: ${op.id}` });
652
- continue;
653
- }
654
- if (op.changes.content !== void 0) entry.content = op.changes.content.trim();
655
- if (op.changes.confidence !== void 0) {
656
- const cap = capForKinds(entry.evidence.map((e) => e.kind));
657
- if (op.changes.confidence > entry.confidence && entry.evidence.length < 2) {
658
- rejected.push({ op, reason: "confidence upgrade requires a second confirming observation" });
659
- continue;
660
- }
661
- entry.confidence = Math.min(op.changes.confidence, cap);
662
- }
663
- entry.updatedAt = nowIso;
664
- entry.updateCount += 1;
665
- applied.push(op);
666
- continue;
667
- }
668
- if (op.op === "INVALIDATE") {
669
- const entry = findActive(doc, op.id);
670
- if (!entry) {
671
- rejected.push({ op, reason: `unknown or inactive entry: ${op.id}` });
672
- continue;
673
- }
674
- if (entry.temporal === "volatile") {
675
- rejected.push({ op, reason: "volatile expiry is code-owned (time-driven), not LLM-nominated" });
676
- continue;
677
- }
678
- const hasNewObservation = (op.evidence ?? []).length > 0 && (op.evidence ?? []).some((e) => parseIso(e.at) > parseIso(entry.evidence[entry.evidence.length - 1]?.at ?? ""));
679
- if (!hasNewObservation) {
680
- rejected.push({ op, reason: "stable INVALIDATE requires a newer contradicting observation" });
681
- continue;
682
- }
683
- entry.validTo = nowIso;
684
- entry.supersededBy = null;
685
- entry.updatedAt = nowIso;
686
- entry.updateCount += 1;
687
- if (op.evidence) entry.evidence.push(...op.evidence);
688
- applied.push(op);
689
- continue;
690
- }
691
- }
692
- return { applied, rejected };
693
- }
694
- var dbSeq = 0;
695
- function runDeterministicAging(doc, policy, now) {
696
- const nowIso = new Date(now).toISOString();
697
- let volatileExpired = 0;
698
- let lowActivityMarked = 0;
699
- for (const p of PARTITIONS) {
700
- for (const e of doc.partitions[p].entries) {
701
- if (e.validTo !== null) continue;
702
- const lastEvidence = Math.max(...e.evidence.map((x) => parseIso(x.at)), parseIso(e.updatedAt));
703
- if (e.temporal === "volatile") {
704
- if (now - lastEvidence > policy.profile.volatileDays * DAY_MS2) {
705
- e.validTo = nowIso;
706
- e.updatedAt = nowIso;
707
- e.updateCount += 1;
708
- volatileExpired += 1;
709
- }
710
- } else if (!e.lowActivity && now - lastEvidence > policy.profile.stableLowActivityDays * DAY_MS2) {
711
- e.lowActivity = true;
712
- lowActivityMarked += 1;
713
- }
494
+ function listWeeklyReports(guard, dataDir) {
495
+ const out = [];
496
+ try {
497
+ for (const file of fs4.readdirSync(weeklyDirPath(dataDir))) {
498
+ if (!/^report-\d{4}-\d{2}-\d{2}\.json$/.test(file)) continue;
499
+ const rep = readWeeklyReport(guard, dataDir, file);
500
+ if (!rep) continue;
501
+ out.push({ file, start: rep.start, end: rep.end, generatedAt: rep.generatedAt, source: rep.source });
714
502
  }
503
+ } catch {
715
504
  }
716
- return { volatileExpired, lowActivityMarked };
505
+ return out.sort((a, b) => a.end < b.end ? 1 : -1);
717
506
  }
718
- function persistWithJournal(guard, dataDir, doc, record) {
719
- saveJson(guard, profileFilePath(dataDir), doc);
720
- const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...record });
721
- const journal = journalFilePath(dataDir);
507
+ function readWeeklyReport(guard, dataDir, file) {
508
+ if (!/^report-\d{4}-\d{2}-\d{2}\.json$/.test(file)) return null;
722
509
  try {
723
- fs7.appendFileSync(guard.assert(journal), line + "\n", "utf8");
510
+ const raw = loadEncryptedText(guard, path6.join(weeklyDirPath(dataDir), file));
511
+ if (!raw) return null;
512
+ const rep = JSON.parse(raw);
513
+ if (typeof rep.text !== "string") return null;
514
+ return rep;
724
515
  } catch {
725
- fs7.mkdirSync(dataDir, { recursive: true });
726
- fs7.appendFileSync(guard.assert(journal), line + "\n", "utf8");
516
+ return null;
727
517
  }
728
518
  }
729
- function applyOpPermissive(doc, op, ts) {
730
- if (op.op === "ADD") {
731
- dbSeq += 1;
732
- doc.partitions[op.partition].entries.push({
733
- id: op.assignedId ?? `r${dbSeq.toString(36)}${randomUUID3().slice(0, 4)}`,
734
- partition: op.partition,
735
- topic: op.topic,
736
- subTopic: op.subTopic,
737
- content: op.content,
738
- confidence: op.confidence ?? 0.5,
739
- temporal: op.temporal ?? "stable",
740
- validFrom: ts,
741
- validTo: null,
742
- supersededBy: null,
743
- evidence: op.evidence,
744
- createdAt: ts,
745
- updatedAt: ts,
746
- updateCount: 0
747
- });
748
- return;
519
+ function buildWeeklyPrompt(facts) {
520
+ return [
521
+ "\u4F60\u662F\u5FC3\u8DF3\u63D2\u4EF6\u7684\u540E\u53F0\u62A5\u544A\u5668\u3002\u6839\u636E\u4E0B\u9762\u7684\u672C\u5468\u4E8B\u5B9E\u6E05\u5355\uFF0C\u7528\u4E2D\u6587\u5199\u4E00\u4EFD\u5468\u62A5\u6B63\u6587\u3002",
522
+ "\u89C4\u5219\uFF1A",
523
+ "- \u53D9\u8FF0\u8005\u81EA\u79F0\u300C\u5FC3\u8DF3\u300D\u2014\u2014\u8FD9\u662F\u63D2\u4EF6\u540E\u53F0\uFF0C\u4E0D\u662F\u4EFB\u4F55\u4F1A\u8BDD\u91CC\u7684 agent\uFF1B\u7EDD\u4E0D\u80FD\u4EE5\u4F1A\u8BDD agent \u7684\u8EAB\u4EFD\u6216\u4EBA\u683C\u81EA\u79F0\u3002",
524
+ "- \u53EA\u62A5\u544A\u4E8B\u5B9E\u6E05\u5355\u91CC\u6709\u7684\u5185\u5BB9\uFF1B\u67D0\u4E00\u8282\u6CA1\u6709\u6570\u636E\u5C31\u6574\u8282\u8DF3\u8FC7\uFF0C\u4E0D\u8981\u7F16\u9020\u3001\u4E0D\u8981\u51D1\u6570\u3002",
525
+ "- \u8BED\u6C14\u5E73\u5B9E\u4E2D\u6027\uFF0C\u4E0D\u7528\u611F\u53F9\u53F7\uFF1B\u7BC7\u5E45\u4E0D\u8D85\u8FC7 250 \u5B57\u3002",
526
+ "- \u5F85\u529E\u79EF\u538B\u90A3\u4E00\u8282\u7528\u5546\u91CF\u7684\u8BED\u6C14\u63D0\u4E00\u53E5\uFF0C\u4E0D\u50AC\u4FC3\u3002",
527
+ "- \u76F4\u63A5\u8F93\u51FA\u5468\u62A5\u6B63\u6587\uFF0C\u4E0D\u8981\u8F93\u51FA\u89E3\u91CA\u3001\u6807\u9898\u6216 JSON\u3002",
528
+ "",
529
+ "\u672C\u5468\u4E8B\u5B9E\uFF08JSON\uFF09\uFF1A",
530
+ JSON.stringify(facts, null, 1)
531
+ ].join("\n");
532
+ }
533
+ function renderTemplateReport(facts) {
534
+ const d = (iso) => iso.slice(0, 10);
535
+ const lines = [`\u672C\u5468\uFF08${d(facts.windowStart)} ~ ${d(facts.windowEnd)}\uFF09`];
536
+ lines.push(`\u8868\u8FBE ${facts.spoken} \u6B21\uFF0C\u9759\u9ED8 ${facts.silent} \u6B21\u3002`);
537
+ if (facts.silentTopReasons.length > 0) {
538
+ lines.push(`\u9759\u9ED8\u4E3B\u56E0\uFF1A${facts.silentTopReasons.map((r) => `${r.reason}\uFF08${r.count}\uFF09`).join("\u3001")}\u3002`);
749
539
  }
750
- if (op.op === "UPDATE") {
751
- const e = [...PARTITIONS].flatMap((p) => doc.partitions[p].entries).find((x) => x.id === op.id);
752
- if (e) {
753
- if (op.changes.content !== void 0) e.content = op.changes.content;
754
- if (op.changes.confidence !== void 0) e.confidence = op.changes.confidence;
755
- e.updatedAt = ts;
756
- e.updateCount += 1;
757
- }
758
- return;
540
+ if (facts.profileAdds.length > 0) {
541
+ lines.push(`\u65B0\u8BA4\u8BC6 ${facts.profileAdds.length} \u6761\uFF1A${facts.profileAdds.map((a) => a.content).join("\uFF1B")}\u3002`);
759
542
  }
760
- if (op.op === "INVALIDATE") {
761
- const e = [...PARTITIONS].flatMap((p) => doc.partitions[p].entries).find((x) => x.id === op.id);
762
- if (e) {
763
- e.validTo = ts;
764
- e.updatedAt = ts;
765
- e.updateCount += 1;
766
- }
543
+ if (facts.ledger.added.length > 0 || facts.ledger.done.length > 0) {
544
+ const parts = [];
545
+ if (facts.ledger.added.length > 0) parts.push(`\u65B0\u589E\u5F85\u529E\uFF1A${facts.ledger.added.join("\u3001")}`);
546
+ if (facts.ledger.done.length > 0) parts.push(`\u5DF2\u89E3\u51B3\uFF1A${facts.ledger.done.join("\u3001")}`);
547
+ lines.push(`${parts.join("\uFF1B")}\u3002`);
767
548
  }
768
- }
769
- function replayJournal(guard, dataDir) {
770
- const journal = journalFilePath(dataDir);
771
- const raw = readText(guard, journal, "");
772
- const doc = emptyProfile();
773
- let records = 0;
774
- let truncatedTail = 0;
775
- const lines = raw.split("\n");
776
- for (let i = 0; i < lines.length; i++) {
777
- const trimmed = lines[i].trim();
778
- if (!trimmed) continue;
779
- try {
780
- const rec = JSON.parse(trimmed);
781
- for (const op of rec.applied ?? []) applyOpPermissive(doc, op, rec.ts);
782
- records += 1;
783
- } catch {
784
- const isLast = lines.slice(i + 1).every((l) => !l.trim());
785
- if (isLast) {
786
- truncatedTail = lines.length - i;
787
- break;
788
- }
789
- }
549
+ if (facts.ledger.stale.length > 0) {
550
+ lines.push(`\u6302\u4E86\u5F88\u4E45\uFF1A${facts.ledger.stale.map((s) => `${s.text}\uFF08${s.days} \u5929\uFF09`).join("\u3001")}\u2014\u2014\u8981\u5220\u6389\u8FD8\u662F\u7EE7\u7EED\u6302\u7740\uFF1F`);
790
551
  }
791
- return { doc, truncatedTail, records };
792
- }
793
- function verifyProfile(guard, dataDir) {
794
- const replayed = replayJournal(guard, dataDir);
795
- const onDisk = loadProfile(guard, profileFilePath(dataDir));
796
- const strip = (doc) => JSON.stringify(doc.partitions, (k, v) => ["id", "supersededBy", "validFrom", "createdAt", "updatedAt", "retiredAt"].includes(k) ? "<norm>" : v);
797
- const ok = strip(replayed.doc) === strip(onDisk);
798
- if (ok) return { ok: true, truncatedTail: replayed.truncatedTail, records: replayed.records };
799
- const diskIds = new Set([...PARTITIONS].flatMap((p) => onDisk.partitions[p].entries.map((e) => e.content)));
800
- const replayIds = new Set([...PARTITIONS].flatMap((p) => replayed.doc.partitions[p].entries.map((e) => e.content)));
801
- const onlyDisk = [...diskIds].find((c) => !replayIds.has(c));
802
- const onlyReplay = [...replayIds].find((c) => !diskIds.has(c));
803
- return {
804
- ok: false,
805
- firstDivergence: {
806
- id: onlyDisk ?? onlyReplay ?? "(content)",
807
- expected: onlyReplay ? "absent in journal replay" : "present in journal replay",
808
- actual: onlyDisk ? "present on disk" : "absent on disk"
809
- },
810
- truncatedTail: replayed.truncatedTail,
811
- records: replayed.records
812
- };
813
- }
814
- function rebuildProfile(guard, dataDir, opts = {}) {
815
- const replayed = replayJournal(guard, dataDir);
816
- const target = profileFilePath(dataDir);
817
- if (opts.check) {
818
- const onDisk = loadProfile(guard, target);
819
- const same = JSON.stringify(onDisk) === JSON.stringify(replayed.doc);
820
- return {
821
- ok: same,
822
- truncatedTail: replayed.truncatedTail,
823
- records: replayed.records,
824
- wrote: false,
825
- diffSummary: same ? "no diff" : "materialized view differs from journal replay"
826
- };
552
+ if (facts.seeds.added.length > 0) {
553
+ lines.push(`\u7D20\u6750\u6C60\u65B0\u589E ${facts.seeds.added.length} \u6761\uFF0C\u5DF2\u6D88\u8D39 ${facts.seeds.consumed.length} \u6761\u3002`);
827
554
  }
828
- saveJson(guard, target, replayed.doc);
829
- if (replayed.truncatedTail > 0) {
830
- writeText(
831
- guard,
832
- path8.join(dataDir, "logs", "rebuild-report.txt"),
833
- `rebuild truncated ${replayed.truncatedTail} torn line(s) at journal tail; ${replayed.records} records applied
834
- `
835
- );
555
+ if (facts.seeds.waiting.length > 0) {
556
+ lines.push(`\u8FD8\u6CA1\u804A\u8FC7\u7684\u7D20\u6750\uFF1A${facts.seeds.waiting.join("\u3001")}\u3002`);
836
557
  }
837
- return { ok: true, truncatedTail: replayed.truncatedTail, records: replayed.records, wrote: true };
838
- }
839
-
840
- // src/notify/notify.ts
841
- import { spawnSync } from "child_process";
842
- import path9 from "path";
843
- function runNotify(paths, args) {
844
- const r = spawnSync(
845
- "powershell.exe",
846
- ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path9.join(paths.assetsDir, "notify.ps1"), ...args],
847
- { timeout: 2e4, encoding: "utf8" }
848
- );
849
- return { status: r.status ?? -1, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
850
- }
851
- function ensureRegistered(paths) {
852
- const check = runNotify(paths, ["-Check"]);
853
- if (/REGISTERED: yes/.test(check.out)) return true;
854
- const reg = runNotify(paths, ["-RegisterOnly"]);
855
- return reg.status === 0;
856
- }
857
- function sendNewMessageHint(paths) {
858
- const r = runNotify(paths, ["-Title", "Heartbeat", "-Message", "\u6709\u65B0\u6D88\u606F"]);
859
- return r.status === 0 && /TOAST_SENT/.test(r.out);
558
+ if (facts.peakHours.length > 0) {
559
+ lines.push(`\u6D3B\u8DC3\u9AD8\u5CF0\uFF1A${facts.peakHours.join("\u3001")}\u3002`);
560
+ }
561
+ return lines.join("\n");
860
562
  }
861
563
 
862
564
  // src/core/preset-install.ts
863
- import fs8 from "fs";
565
+ import fs5 from "fs";
864
566
  import os from "os";
865
- import path10 from "path";
567
+ import path7 from "path";
866
568
  import { fileURLToPath } from "url";
867
569
  var COMPOSITION_FILE = "agent.cordis.yml";
868
570
  var METADATA_FILE = "preset.yml";
@@ -870,14 +572,14 @@ var BUNDLED_PRESET_ID = "heartbeat";
870
572
  function bundledPresetDir(moduleUrl, id = BUNDLED_PRESET_ID) {
871
573
  let dir;
872
574
  try {
873
- dir = path10.dirname(fileURLToPath(moduleUrl));
575
+ dir = path7.dirname(fileURLToPath(moduleUrl));
874
576
  } catch {
875
577
  return void 0;
876
578
  }
877
579
  for (let depth = 0; depth < 5; depth += 1) {
878
- const candidate = path10.join(dir, "assets", "presets", id);
879
- if (fs8.existsSync(path10.join(candidate, COMPOSITION_FILE))) return candidate;
880
- const parent = path10.dirname(dir);
580
+ const candidate = path7.join(dir, "assets", "presets", id);
581
+ if (fs5.existsSync(path7.join(candidate, COMPOSITION_FILE))) return candidate;
582
+ const parent = path7.dirname(dir);
881
583
  if (parent === dir) break;
882
584
  dir = parent;
883
585
  }
@@ -887,12 +589,12 @@ function userPresetRoot(roots) {
887
589
  const found = roots?.find(
888
590
  (root) => root?.trust === "user" && typeof root.path === "string" && root.path.length > 0
889
591
  );
890
- return found?.path === void 0 ? void 0 : path10.resolve(found.path);
592
+ return found?.path === void 0 ? void 0 : path7.resolve(found.path);
891
593
  }
892
594
  function conventionalUserPresetRoot(env = process.env, home = os.homedir()) {
893
595
  const override = env.DSH_HOME?.trim();
894
- const root = override && override.length > 0 ? override : path10.join(home, ".dsh");
895
- return path10.join(root, ".agent-presets");
596
+ const root = override && override.length > 0 ? override : path7.join(home, ".dsh");
597
+ return path7.join(root, ".agent-presets");
896
598
  }
897
599
  function installBundledPreset(options) {
898
600
  const id = options.id && options.id.length > 0 ? options.id : BUNDLED_PRESET_ID;
@@ -924,12 +626,12 @@ function installBundledPreset(options) {
924
626
  detail: "the roster mounts no user preset root (includeUserRoot=false)"
925
627
  };
926
628
  }
927
- const dir = path10.join(root, id);
928
- const composition = path10.join(dir, COMPOSITION_FILE);
629
+ const dir = path7.join(root, id);
630
+ const composition = path7.join(dir, COMPOSITION_FILE);
929
631
  try {
930
- if (fs8.existsSync(composition)) {
632
+ if (fs5.existsSync(composition)) {
931
633
  if (options.force !== true) {
932
- const drifted = !sameBytes(composition, path10.join(bundledDir, COMPOSITION_FILE));
634
+ const drifted = !sameBytes(composition, path7.join(bundledDir, COMPOSITION_FILE));
933
635
  return {
934
636
  action: "exists",
935
637
  id,
@@ -938,7 +640,7 @@ function installBundledPreset(options) {
938
640
  detail: drifted ? "kept as-is (differs from the bundled template)" : "kept as-is"
939
641
  };
940
642
  }
941
- fs8.copyFileSync(path10.join(bundledDir, COMPOSITION_FILE), composition);
643
+ fs5.copyFileSync(path7.join(bundledDir, COMPOSITION_FILE), composition);
942
644
  return {
943
645
  action: "restored",
944
646
  id,
@@ -947,11 +649,11 @@ function installBundledPreset(options) {
947
649
  detail: "composition replaced from the bundled template"
948
650
  };
949
651
  }
950
- const existed = fs8.existsSync(dir);
951
- fs8.mkdirSync(dir, { recursive: true });
952
- fs8.copyFileSync(path10.join(bundledDir, COMPOSITION_FILE), composition);
953
- const metadata = path10.join(dir, METADATA_FILE);
954
- if (!fs8.existsSync(metadata)) fs8.copyFileSync(path10.join(bundledDir, METADATA_FILE), metadata);
652
+ const existed = fs5.existsSync(dir);
653
+ fs5.mkdirSync(dir, { recursive: true });
654
+ fs5.copyFileSync(path7.join(bundledDir, COMPOSITION_FILE), composition);
655
+ const metadata = path7.join(dir, METADATA_FILE);
656
+ if (!fs5.existsSync(metadata)) fs5.copyFileSync(path7.join(bundledDir, METADATA_FILE), metadata);
955
657
  return {
956
658
  action: existed ? "repaired" : "created",
957
659
  id,
@@ -969,21 +671,21 @@ function describeInstall(result) {
969
671
  return `preset ${result.id} ${result.action}${where}${why}`;
970
672
  }
971
673
  function presetStatus(moduleUrl, id = BUNDLED_PRESET_ID, root = conventionalUserPresetRoot()) {
972
- const dir = path10.join(root, id);
674
+ const dir = path7.join(root, id);
973
675
  const bundledDir = bundledPresetDir(moduleUrl, id);
974
- const installed = fs8.existsSync(path10.join(dir, COMPOSITION_FILE));
676
+ const installed = fs5.existsSync(path7.join(dir, COMPOSITION_FILE));
975
677
  return {
976
678
  id,
977
679
  dir,
978
680
  ...bundledDir === void 0 ? {} : { bundledDir },
979
681
  installed,
980
- compositionMatches: installed && bundledDir !== void 0 && sameBytes(path10.join(dir, COMPOSITION_FILE), path10.join(bundledDir, COMPOSITION_FILE)),
981
- metadataMatches: bundledDir !== void 0 && fs8.existsSync(path10.join(dir, METADATA_FILE)) && sameBytes(path10.join(dir, METADATA_FILE), path10.join(bundledDir, METADATA_FILE))
682
+ compositionMatches: installed && bundledDir !== void 0 && sameBytes(path7.join(dir, COMPOSITION_FILE), path7.join(bundledDir, COMPOSITION_FILE)),
683
+ metadataMatches: bundledDir !== void 0 && fs5.existsSync(path7.join(dir, METADATA_FILE)) && sameBytes(path7.join(dir, METADATA_FILE), path7.join(bundledDir, METADATA_FILE))
982
684
  };
983
685
  }
984
686
  function sameBytes(left, right) {
985
687
  try {
986
- return fs8.readFileSync(left).equals(fs8.readFileSync(right));
688
+ return fs5.readFileSync(left).equals(fs5.readFileSync(right));
987
689
  } catch {
988
690
  return false;
989
691
  }
@@ -991,10 +693,6 @@ function sameBytes(left, right) {
991
693
 
992
694
  export {
993
695
  createPathGuard,
994
- atomicWriteJsonSync,
995
- shredFileSync,
996
- appendAuditLine,
997
- pruneAuditFile,
998
696
  deepMerge,
999
697
  loadPolicy,
1000
698
  updateUserPolicy,
@@ -1011,16 +709,16 @@ export {
1011
709
  completeWander,
1012
710
  adviseRefillWander,
1013
711
  browseStatus,
1014
- loadProfileSchema,
1015
- profileFilePath,
1016
- loadProfile,
1017
- applyOpsToDoc,
1018
- runDeterministicAging,
1019
- persistWithJournal,
1020
- verifyProfile,
1021
- rebuildProfile,
1022
712
  ensureRegistered,
1023
713
  sendNewMessageHint,
714
+ sendWeeklyReadyHint,
715
+ weeklyDirPath,
716
+ weeklyDue,
717
+ saveWeeklyReport,
718
+ listWeeklyReports,
719
+ readWeeklyReport,
720
+ buildWeeklyPrompt,
721
+ renderTemplateReport,
1024
722
  BUNDLED_PRESET_ID,
1025
723
  userPresetRoot,
1026
724
  conventionalUserPresetRoot,
@@ -1028,4 +726,4 @@ export {
1028
726
  describeInstall,
1029
727
  presetStatus
1030
728
  };
1031
- //# sourceMappingURL=chunk-ON4MSU6E.js.map
729
+ //# sourceMappingURL=chunk-SQJLKP7L.js.map