@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/README.md +52 -15
- package/client.js +93 -0
- package/config/policy.json +73 -64
- package/dist/{chunk-ON4MSU6E.js → chunk-2SYXKKZF.js} +286 -480
- package/dist/chunk-2SYXKKZF.js.map +1 -0
- package/dist/{chunk-4UE74TUB.js → chunk-63FUJLIF.js} +2 -2
- package/dist/{chunk-LLD7LUNN.js → chunk-IFTFDHZX.js} +2 -1
- package/dist/chunk-IPOZFZPY.js +177 -0
- package/dist/chunk-IPOZFZPY.js.map +1 -0
- package/dist/{chunk-SVP2NDRF.js → chunk-PQTSXW4F.js} +38 -9
- package/dist/chunk-PQTSXW4F.js.map +1 -0
- package/dist/chunk-RB3NLLGF.js +87 -0
- package/dist/chunk-RB3NLLGF.js.map +1 -0
- package/dist/chunk-VIZNIQLK.js +472 -0
- package/dist/chunk-VIZNIQLK.js.map +1 -0
- package/dist/cli/index.js +149 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/{inbox-MMLHISQV.js → inbox-57NHQHY3.js} +3 -3
- package/dist/index.js +790 -209
- package/dist/index.js.map +1 -1
- package/dist/migrate-N6NXMZ3G.js +20 -0
- package/dist/{pool-YAIBE2L5.js → pool-7I4VACG2.js} +3 -3
- package/dist/pool-7I4VACG2.js.map +1 -0
- package/dist/snapshot-ZBCSX67Y.js +15 -0
- package/dist/snapshot-ZBCSX67Y.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-ON4MSU6E.js.map +0 -1
- package/dist/chunk-SVP2NDRF.js.map +0 -1
- /package/dist/{chunk-4UE74TUB.js.map → chunk-63FUJLIF.js.map} +0 -0
- /package/dist/{chunk-LLD7LUNN.js.map → chunk-IFTFDHZX.js.map} +0 -0
- /package/dist/{inbox-MMLHISQV.js.map → inbox-57NHQHY3.js.map} +0 -0
- /package/dist/{pool-YAIBE2L5.js.map → migrate-N6NXMZ3G.js.map} +0 -0
|
@@ -3,13 +3,18 @@ import {
|
|
|
3
3
|
loadPool,
|
|
4
4
|
normalizeCategory,
|
|
5
5
|
seedsFilePath
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-PQTSXW4F.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-
|
|
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");
|
|
@@ -190,6 +122,7 @@ function assertPolicy(input) {
|
|
|
190
122
|
if (typeof s.ttlDays[k] !== "number") fail(`seeds.ttlDays.${k} missing`);
|
|
191
123
|
}
|
|
192
124
|
if (typeof s.coldBenchDays !== "number") fail("seeds.coldBenchDays missing");
|
|
125
|
+
if (typeof s.archiveCap !== "number" || s.archiveCap < 1) fail("seeds.archiveCap must be >= 1");
|
|
193
126
|
if (typeof s.retireAfterUsed !== "number" || s.retireAfterUsed < 1) fail("seeds.retireAfterUsed must be >= 1");
|
|
194
127
|
if (!isPlainObject(s.scoreWeights)) fail("seeds.scoreWeights missing");
|
|
195
128
|
const pr = p.profile;
|
|
@@ -207,6 +140,12 @@ function assertPolicy(input) {
|
|
|
207
140
|
if (typeof pr.volatileDays !== "number" || pr.volatileDays < 1) fail("profile.volatileDays must be >= 1");
|
|
208
141
|
if (typeof pr.stableLowActivityDays !== "number" || pr.stableLowActivityDays < 1) fail("profile.stableLowActivityDays must be >= 1");
|
|
209
142
|
if (typeof pr.psyEnabled !== "boolean") fail("profile.psyEnabled must be boolean");
|
|
143
|
+
const ob = p.observe;
|
|
144
|
+
if (!isPlainObject(ob)) fail("observe missing");
|
|
145
|
+
if (typeof ob.maxChars !== "number" || ob.maxChars < 20 || ob.maxChars > 2e3) fail("observe.maxChars must be in [20, 2000]");
|
|
146
|
+
if (typeof ob.perBeat !== "number" || ob.perBeat < 1 || ob.perBeat > 100) fail("observe.perBeat must be in [1, 100]");
|
|
147
|
+
if (!isPlainObject(p.weekly)) fail("weekly missing");
|
|
148
|
+
if (typeof p.weekly.enabled !== "boolean") fail("weekly.enabled must be boolean");
|
|
210
149
|
const r = p.retention;
|
|
211
150
|
if (!isPlainObject(r)) fail("retention missing");
|
|
212
151
|
if (typeof r.envPulseHours !== "number" || typeof r.decisionLogDays !== "number") fail("retention fields missing");
|
|
@@ -223,23 +162,23 @@ function deepMerge(base, override) {
|
|
|
223
162
|
}
|
|
224
163
|
|
|
225
164
|
// src/config/load.ts
|
|
226
|
-
import
|
|
227
|
-
import
|
|
165
|
+
import fs2 from "fs";
|
|
166
|
+
import path2 from "path";
|
|
228
167
|
var USER_POLICY_FILE = "policy.json";
|
|
229
168
|
function loadPolicy(guard, configDir, settingsDir) {
|
|
230
|
-
const factoryPath =
|
|
169
|
+
const factoryPath = path2.join(configDir, "policy.json");
|
|
231
170
|
let factoryRaw;
|
|
232
171
|
try {
|
|
233
|
-
factoryRaw = JSON.parse(
|
|
172
|
+
factoryRaw = JSON.parse(fs2.readFileSync(factoryPath, "utf8"));
|
|
234
173
|
} catch (e) {
|
|
235
174
|
throw new Error(`factory policy unreadable at ${factoryPath}: ${String(e)}`);
|
|
236
175
|
}
|
|
237
176
|
assertPolicy(factoryRaw);
|
|
238
|
-
const userPath = guard.assert(
|
|
177
|
+
const userPath = guard.assert(path2.join(settingsDir, USER_POLICY_FILE));
|
|
239
178
|
let merged = factoryRaw;
|
|
240
|
-
if (
|
|
179
|
+
if (fs2.existsSync(userPath)) {
|
|
241
180
|
try {
|
|
242
|
-
const userRaw = JSON.parse(
|
|
181
|
+
const userRaw = JSON.parse(fs2.readFileSync(userPath, "utf8"));
|
|
243
182
|
merged = deepMerge(factoryRaw, userRaw);
|
|
244
183
|
} catch (e) {
|
|
245
184
|
throw new Error(`user policy layer unparseable at ${userPath}: ${String(e)}`);
|
|
@@ -249,26 +188,26 @@ function loadPolicy(guard, configDir, settingsDir) {
|
|
|
249
188
|
return merged;
|
|
250
189
|
}
|
|
251
190
|
function updateUserPolicy(guard, settingsDir, patch) {
|
|
252
|
-
const userPath = guard.assert(
|
|
191
|
+
const userPath = guard.assert(path2.join(settingsDir, USER_POLICY_FILE));
|
|
253
192
|
let user = {};
|
|
254
193
|
try {
|
|
255
|
-
user = JSON.parse(
|
|
194
|
+
user = JSON.parse(fs2.readFileSync(userPath, "utf8"));
|
|
256
195
|
if (!user || typeof user !== "object" || Array.isArray(user)) user = {};
|
|
257
196
|
} catch {
|
|
258
197
|
user = {};
|
|
259
198
|
}
|
|
260
199
|
const merged = deepMerge(user, patch);
|
|
261
|
-
|
|
262
|
-
|
|
200
|
+
fs2.mkdirSync(path2.dirname(userPath), { recursive: true });
|
|
201
|
+
fs2.writeFileSync(userPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
263
202
|
return merged;
|
|
264
203
|
}
|
|
265
204
|
|
|
266
205
|
// src/ledger/ledger.ts
|
|
267
|
-
import
|
|
268
|
-
import { randomUUID
|
|
206
|
+
import path3 from "path";
|
|
207
|
+
import { randomUUID } from "crypto";
|
|
269
208
|
var DAY_MS = 864e5;
|
|
270
209
|
function ledgerFilePath(dataDir) {
|
|
271
|
-
return
|
|
210
|
+
return path3.join(dataDir, "ledger.md");
|
|
272
211
|
}
|
|
273
212
|
var LINE_RE = /^- \[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})\]\[(open|done)\]\[#([0-9a-f]{6})\] (.*)$/;
|
|
274
213
|
function renderEntry(e) {
|
|
@@ -294,7 +233,7 @@ function appendEntry(guard, file, text, now = Date.now()) {
|
|
|
294
233
|
const d = new Date(now);
|
|
295
234
|
const pad = (n) => String(n).padStart(2, "0");
|
|
296
235
|
const entry = {
|
|
297
|
-
id:
|
|
236
|
+
id: randomUUID().slice(0, 6),
|
|
298
237
|
date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,
|
|
299
238
|
time: `${pad(d.getHours())}:${pad(d.getMinutes())}`,
|
|
300
239
|
status: "open",
|
|
@@ -311,9 +250,10 @@ function markDone(guard, file, key, now = Date.now()) {
|
|
|
311
250
|
if (!target) return null;
|
|
312
251
|
const d = new Date(now);
|
|
313
252
|
const pad = (n) => String(n).padStart(2, "0");
|
|
253
|
+
const doneDate = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
314
254
|
const out = rawLines.map((line) => {
|
|
315
255
|
if (line.includes(`#${target.id}] `)) {
|
|
316
|
-
return `- [${
|
|
256
|
+
return `- [${doneDate} ${pad(d.getHours())}:${pad(d.getMinutes())}][done][#${target.id}] ${target.text}`;
|
|
317
257
|
}
|
|
318
258
|
return line;
|
|
319
259
|
});
|
|
@@ -330,32 +270,111 @@ function pendingOlderThan(guard, file, days, now = Date.now()) {
|
|
|
330
270
|
}
|
|
331
271
|
|
|
332
272
|
// src/browse/browse.ts
|
|
333
|
-
import
|
|
334
|
-
import
|
|
273
|
+
import fs3 from "fs";
|
|
274
|
+
import path5 from "path";
|
|
275
|
+
|
|
276
|
+
// src/browse/preference.ts
|
|
277
|
+
import path4 from "path";
|
|
278
|
+
var DAY_MS2 = 864e5;
|
|
279
|
+
var W_MIN = 0.5;
|
|
280
|
+
var W_MAX = 2;
|
|
281
|
+
var SMOOTH_ADOPTED = 1;
|
|
282
|
+
var SMOOTH_DELIVERED = 4;
|
|
283
|
+
var DECAY_HALF_LIFE_DAYS = 30;
|
|
284
|
+
function preferenceFilePath(dataDir) {
|
|
285
|
+
return path4.join(dataDir, "preference.json");
|
|
286
|
+
}
|
|
287
|
+
function num(v) {
|
|
288
|
+
const n = Number(v);
|
|
289
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
290
|
+
}
|
|
291
|
+
function sanitizeTopicPref(raw) {
|
|
292
|
+
if (!raw || typeof raw !== "object") return null;
|
|
293
|
+
const t = raw;
|
|
294
|
+
if (!Number.isFinite(Number(t.delivered)) || !Number.isFinite(Number(t.adopted))) return null;
|
|
295
|
+
const delivered = Math.floor(num(t.delivered));
|
|
296
|
+
return {
|
|
297
|
+
delivered,
|
|
298
|
+
adopted: Math.min(delivered, Math.floor(num(t.adopted))),
|
|
299
|
+
lastDeliveredAt: num(t.lastDeliveredAt),
|
|
300
|
+
lastAdoptedAt: num(t.lastAdoptedAt)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function loadPreference(guard, file) {
|
|
304
|
+
const doc = loadJson(guard, file);
|
|
305
|
+
if (!doc || typeof doc !== "object" || !doc.topics || typeof doc.topics !== "object") {
|
|
306
|
+
return { version: 1, topics: {}, updatedAt: 0 };
|
|
307
|
+
}
|
|
308
|
+
const topics = {};
|
|
309
|
+
for (const [key, raw] of Object.entries(doc.topics)) {
|
|
310
|
+
const t = sanitizeTopicPref(raw);
|
|
311
|
+
if (key && t) topics[key] = t;
|
|
312
|
+
}
|
|
313
|
+
const updatedAt = Number(doc.updatedAt);
|
|
314
|
+
return { version: 1, topics, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };
|
|
315
|
+
}
|
|
316
|
+
function savePreference(guard, file, state) {
|
|
317
|
+
saveJson(guard, file, state);
|
|
318
|
+
}
|
|
319
|
+
function recordDelivery(guard, file, opts) {
|
|
320
|
+
const state = loadPreference(guard, file);
|
|
321
|
+
const adopted = new Set(opts.adoptedTopics);
|
|
322
|
+
for (const topic of opts.offeredTopics) {
|
|
323
|
+
if (!topic) continue;
|
|
324
|
+
const t = state.topics[topic] ?? { delivered: 0, adopted: 0, lastDeliveredAt: 0, lastAdoptedAt: 0 };
|
|
325
|
+
t.delivered += 1;
|
|
326
|
+
t.lastDeliveredAt = opts.now;
|
|
327
|
+
if (adopted.has(topic)) {
|
|
328
|
+
t.adopted += 1;
|
|
329
|
+
t.lastAdoptedAt = opts.now;
|
|
330
|
+
}
|
|
331
|
+
state.topics[topic] = t;
|
|
332
|
+
}
|
|
333
|
+
state.updatedAt = opts.now;
|
|
334
|
+
savePreference(guard, file, state);
|
|
335
|
+
return state;
|
|
336
|
+
}
|
|
337
|
+
function decayed(t, now) {
|
|
338
|
+
const dAgeDays = Math.max(0, now - t.lastDeliveredAt) / DAY_MS2;
|
|
339
|
+
const aAgeDays = Math.max(0, now - t.lastAdoptedAt) / DAY_MS2;
|
|
340
|
+
const dHalf = Math.pow(0.5, dAgeDays / DECAY_HALF_LIFE_DAYS);
|
|
341
|
+
const aHalf = Math.pow(0.5, aAgeDays / DECAY_HALF_LIFE_DAYS);
|
|
342
|
+
return { delivered: t.delivered * dHalf, adopted: t.adopted * aHalf };
|
|
343
|
+
}
|
|
344
|
+
function preferenceWeight(state, topic, now) {
|
|
345
|
+
const t = state.topics[topic];
|
|
346
|
+
const rate = t ? (() => {
|
|
347
|
+
const d = decayed(t, now);
|
|
348
|
+
return (d.adopted + SMOOTH_ADOPTED) / (d.delivered + SMOOTH_DELIVERED);
|
|
349
|
+
})() : SMOOTH_ADOPTED / SMOOTH_DELIVERED;
|
|
350
|
+
return Math.min(W_MAX, Math.max(W_MIN, rate / (SMOOTH_ADOPTED / SMOOTH_DELIVERED)));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/browse/browse.ts
|
|
335
354
|
var WATCH_THROTTLE_MS = 6 * 36e5;
|
|
336
355
|
var UA = { "User-Agent": "dsh-heartbeat/2.0 (+local; personal companion)" };
|
|
337
356
|
function emptyBrowseState() {
|
|
338
357
|
return { targets: {}, last_check_at: 0, wander: { focusHistory: {}, focusCount: {}, last_wander_at: 0, refillCount: {} } };
|
|
339
358
|
}
|
|
340
359
|
function browseStatePath(paths) {
|
|
341
|
-
return
|
|
360
|
+
return path5.join(paths.dataDir, "browse.json");
|
|
342
361
|
}
|
|
343
362
|
function readJsonFile(file, fallback) {
|
|
344
363
|
try {
|
|
345
|
-
return JSON.parse(
|
|
364
|
+
return JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
346
365
|
} catch {
|
|
347
366
|
return fallback;
|
|
348
367
|
}
|
|
349
368
|
}
|
|
350
369
|
function loadInterests(paths) {
|
|
351
|
-
const userPath =
|
|
352
|
-
if (
|
|
353
|
-
return readJsonFile(
|
|
370
|
+
const userPath = path5.join(paths.settingsDir, "interests.json");
|
|
371
|
+
if (fs3.existsSync(userPath)) return readJsonFile(userPath, { interests: [], _schedule: {} });
|
|
372
|
+
return readJsonFile(path5.join(paths.configDir, "interests.json"), { interests: [], _schedule: {} });
|
|
354
373
|
}
|
|
355
374
|
function loadWatchlist(paths) {
|
|
356
|
-
const userPath =
|
|
357
|
-
if (
|
|
358
|
-
return readJsonFile(
|
|
375
|
+
const userPath = path5.join(paths.settingsDir, "watchlist.json");
|
|
376
|
+
if (fs3.existsSync(userPath)) return readJsonFile(userPath, { targets: [] });
|
|
377
|
+
return readJsonFile(path5.join(paths.configDir, "watchlist.json"), { targets: [] });
|
|
359
378
|
}
|
|
360
379
|
function loadState(guard, paths) {
|
|
361
380
|
return loadJson(guard, browseStatePath(paths)) ?? emptyBrowseState();
|
|
@@ -423,12 +442,24 @@ function onCooldown(state, focus, cooldownDays, now) {
|
|
|
423
442
|
const last = state.wander.focusHistory[focus] ?? 0;
|
|
424
443
|
return last > now - cooldownDays * 864e5;
|
|
425
444
|
}
|
|
426
|
-
|
|
445
|
+
var STARVATION_DAYS = 14;
|
|
446
|
+
function pickFocus(state, interests, now, pref) {
|
|
427
447
|
const sc = interests._schedule ?? {};
|
|
428
448
|
const cooldown = sc.focus_cooldown_days ?? 3;
|
|
429
449
|
const pool = (interests.interests ?? []).filter((t) => !onCooldown(state, t, cooldown, now));
|
|
430
450
|
if (pool.length === 0) return null;
|
|
431
|
-
|
|
451
|
+
if (!pref) {
|
|
452
|
+
pool.sort((a, b) => (state.wander.focusHistory[a] ?? 0) - (state.wander.focusHistory[b] ?? 0));
|
|
453
|
+
return pool[0];
|
|
454
|
+
}
|
|
455
|
+
const age = (t) => now - (state.wander.focusHistory[t] ?? 0);
|
|
456
|
+
const starved = pool.filter((t) => age(t) >= STARVATION_DAYS * 864e5);
|
|
457
|
+
if (starved.length > 0) {
|
|
458
|
+
starved.sort((a, b) => age(b) - age(a));
|
|
459
|
+
return starved[0];
|
|
460
|
+
}
|
|
461
|
+
const effAge = (t) => age(t) * preferenceWeight(pref, t, now);
|
|
462
|
+
pool.sort((a, b) => effAge(b) - effAge(a));
|
|
432
463
|
return pool[0];
|
|
433
464
|
}
|
|
434
465
|
function adviseWander(guard, paths, policy, now = /* @__PURE__ */ new Date()) {
|
|
@@ -444,7 +475,7 @@ function adviseWander(guard, paths, policy, now = /* @__PURE__ */ new Date()) {
|
|
|
444
475
|
if (now.getTime() - state.wander.last_wander_at < minGap) {
|
|
445
476
|
return { focus: null, query: null, skipped: "min-interval" };
|
|
446
477
|
}
|
|
447
|
-
const focus = pickFocus(state, interests, now.getTime());
|
|
478
|
+
const focus = pickFocus(state, interests, now.getTime(), loadPreference(guard, preferenceFilePath(paths.dataDir)));
|
|
448
479
|
if (!focus) return { focus: null, query: null, skipped: "no-focus" };
|
|
449
480
|
return { focus, query: `${focus} 2026 \u6700\u65B0`, skipped: null };
|
|
450
481
|
}
|
|
@@ -483,7 +514,7 @@ function adviseRefillWander(guard, paths, policy, now = /* @__PURE__ */ new Date
|
|
|
483
514
|
return { focus: null, query: null, skipped: `topic-stock-ok(${topicCount})`, topicCount, refillsToday };
|
|
484
515
|
}
|
|
485
516
|
const interests = loadInterests(paths);
|
|
486
|
-
const focus = pickFocus(state, interests, now.getTime());
|
|
517
|
+
const focus = pickFocus(state, interests, now.getTime(), loadPreference(guard, preferenceFilePath(paths.dataDir)));
|
|
487
518
|
if (!focus) {
|
|
488
519
|
return { focus: null, query: null, skipped: "no-focus", topicCount, refillsToday };
|
|
489
520
|
}
|
|
@@ -493,376 +524,152 @@ function browseStatus(guard, paths) {
|
|
|
493
524
|
return loadState(guard, paths);
|
|
494
525
|
}
|
|
495
526
|
|
|
496
|
-
// src/
|
|
497
|
-
import
|
|
498
|
-
import
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
};
|
|
527
|
+
// src/notify/notify.ts
|
|
528
|
+
import { spawnSync } from "child_process";
|
|
529
|
+
import path6 from "path";
|
|
530
|
+
function runNotify(paths, args) {
|
|
531
|
+
const r = spawnSync(
|
|
532
|
+
"powershell.exe",
|
|
533
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path6.join(paths.assetsDir, "notify.ps1"), ...args],
|
|
534
|
+
{ timeout: 2e4, encoding: "utf8" }
|
|
535
|
+
);
|
|
536
|
+
return { status: r.status ?? -1, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
515
537
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
}
|
|
538
|
+
function ensureRegistered(paths) {
|
|
539
|
+
const check = runNotify(paths, ["-Check"]);
|
|
540
|
+
if (/REGISTERED: yes/.test(check.out)) return true;
|
|
541
|
+
const reg = runNotify(paths, ["-RegisterOnly"]);
|
|
542
|
+
return reg.status === 0;
|
|
530
543
|
}
|
|
531
|
-
function
|
|
532
|
-
const
|
|
533
|
-
|
|
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 };
|
|
544
|
+
function sendNewMessageHint(paths) {
|
|
545
|
+
const r = runNotify(paths, ["-Title", "Heartbeat", "-Message", "\u6709\u65B0\u6D88\u606F"]);
|
|
546
|
+
return r.status === 0 && /TOAST_SENT/.test(r.out);
|
|
549
547
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
function profileFilePath(dataDir) {
|
|
554
|
-
return path8.join(dataDir, "profile.json");
|
|
548
|
+
function sendWeeklyReadyHint(paths) {
|
|
549
|
+
const r = runNotify(paths, ["-Title", "\u5FC3\u8DF3\u5468\u62A5", "-Message", "\u672C\u671F\u5468\u62A5\u5DF2\u751F\u6210"]);
|
|
550
|
+
return r.status === 0 && /TOAST_SENT/.test(r.out);
|
|
555
551
|
}
|
|
556
|
-
|
|
557
|
-
|
|
552
|
+
|
|
553
|
+
// src/weekly/report.ts
|
|
554
|
+
import fs4 from "fs";
|
|
555
|
+
import path7 from "path";
|
|
556
|
+
var WEEKLY_INTERVAL_MS = 7 * 864e5;
|
|
557
|
+
function weeklyDirPath(dataDir) {
|
|
558
|
+
return path7.join(dataDir, "weekly");
|
|
558
559
|
}
|
|
559
|
-
function
|
|
560
|
-
|
|
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;
|
|
560
|
+
function stateFilePath(dataDir) {
|
|
561
|
+
return path7.join(weeklyDirPath(dataDir), "state.json");
|
|
566
562
|
}
|
|
567
|
-
function
|
|
568
|
-
|
|
569
|
-
return Number.isFinite(t) ? t : 0;
|
|
563
|
+
function reportFileName(endIso) {
|
|
564
|
+
return `report-${endIso.slice(0, 10)}.json`;
|
|
570
565
|
}
|
|
571
|
-
function
|
|
572
|
-
const base = ref.split("#")[0] ?? "";
|
|
573
|
-
if (!base) return false;
|
|
574
|
-
const target = path8.join(dataDir, base);
|
|
566
|
+
function lastWeeklyGeneratedAt(guard, dataDir) {
|
|
575
567
|
try {
|
|
576
|
-
|
|
568
|
+
const raw = JSON.parse(fs4.readFileSync(guard.assert(stateFilePath(dataDir)), "utf8"));
|
|
569
|
+
return Number(raw.lastGeneratedAt) || 0;
|
|
577
570
|
} catch {
|
|
578
|
-
return
|
|
571
|
+
return 0;
|
|
579
572
|
}
|
|
580
573
|
}
|
|
581
|
-
function
|
|
582
|
-
|
|
583
|
-
return
|
|
574
|
+
function weeklyDue(guard, dataDir, now = Date.now()) {
|
|
575
|
+
const last = lastWeeklyGeneratedAt(guard, dataDir);
|
|
576
|
+
return now - last >= WEEKLY_INTERVAL_MS;
|
|
584
577
|
}
|
|
585
|
-
function
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
return void 0;
|
|
578
|
+
function ensureWeeklyAnchor(guard, dataDir, now = Date.now()) {
|
|
579
|
+
if (lastWeeklyGeneratedAt(guard, dataDir) > 0) return false;
|
|
580
|
+
fs4.mkdirSync(weeklyDirPath(dataDir), { recursive: true });
|
|
581
|
+
atomicWriteJsonSync(stateFilePath(dataDir), { lastGeneratedAt: now });
|
|
582
|
+
return true;
|
|
591
583
|
}
|
|
592
|
-
function
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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 };
|
|
584
|
+
function saveWeeklyReport(guard, dataDir, report) {
|
|
585
|
+
const dir = weeklyDirPath(dataDir);
|
|
586
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
587
|
+
const file = path7.join(dir, reportFileName(report.end));
|
|
588
|
+
saveEncryptedText(guard, file, JSON.stringify(report, null, 1));
|
|
589
|
+
atomicWriteJsonSync(stateFilePath(dataDir), { lastGeneratedAt: Date.parse(report.generatedAt) });
|
|
590
|
+
return file;
|
|
693
591
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
}
|
|
592
|
+
function listWeeklyReports(guard, dataDir) {
|
|
593
|
+
const out = [];
|
|
594
|
+
try {
|
|
595
|
+
for (const file of fs4.readdirSync(weeklyDirPath(dataDir))) {
|
|
596
|
+
if (!/^report-\d{4}-\d{2}-\d{2}\.json$/.test(file)) continue;
|
|
597
|
+
const rep = readWeeklyReport(guard, dataDir, file);
|
|
598
|
+
if (!rep) continue;
|
|
599
|
+
out.push({ file, start: rep.start, end: rep.end, generatedAt: rep.generatedAt, source: rep.source });
|
|
714
600
|
}
|
|
601
|
+
} catch {
|
|
715
602
|
}
|
|
716
|
-
return
|
|
603
|
+
return out.sort((a, b) => a.end < b.end ? 1 : -1);
|
|
717
604
|
}
|
|
718
|
-
function
|
|
719
|
-
|
|
720
|
-
const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...record });
|
|
721
|
-
const journal = journalFilePath(dataDir);
|
|
605
|
+
function readWeeklyReport(guard, dataDir, file) {
|
|
606
|
+
if (!/^report-\d{4}-\d{2}-\d{2}\.json$/.test(file)) return null;
|
|
722
607
|
try {
|
|
723
|
-
|
|
608
|
+
const raw = loadEncryptedText(guard, path7.join(weeklyDirPath(dataDir), file));
|
|
609
|
+
if (!raw) return null;
|
|
610
|
+
const rep = JSON.parse(raw);
|
|
611
|
+
if (typeof rep.text !== "string") return null;
|
|
612
|
+
return rep;
|
|
724
613
|
} catch {
|
|
725
|
-
|
|
726
|
-
fs7.appendFileSync(guard.assert(journal), line + "\n", "utf8");
|
|
614
|
+
return null;
|
|
727
615
|
}
|
|
728
616
|
}
|
|
729
|
-
function
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
617
|
+
function buildWeeklyPrompt(facts) {
|
|
618
|
+
return [
|
|
619
|
+
"\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",
|
|
620
|
+
"\u89C4\u5219\uFF1A",
|
|
621
|
+
"- \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",
|
|
622
|
+
"- \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",
|
|
623
|
+
"- \u8BED\u6C14\u5E73\u5B9E\u4E2D\u6027\uFF0C\u4E0D\u7528\u611F\u53F9\u53F7\uFF1B\u7BC7\u5E45\u4E0D\u8D85\u8FC7 250 \u5B57\u3002",
|
|
624
|
+
"- \u5F85\u529E\u79EF\u538B\u90A3\u4E00\u8282\u7528\u5546\u91CF\u7684\u8BED\u6C14\u63D0\u4E00\u53E5\uFF0C\u4E0D\u50AC\u4FC3\u3002",
|
|
625
|
+
"- \u76F4\u63A5\u8F93\u51FA\u5468\u62A5\u6B63\u6587\uFF0C\u4E0D\u8981\u8F93\u51FA\u89E3\u91CA\u3001\u6807\u9898\u6216 JSON\u3002",
|
|
626
|
+
"",
|
|
627
|
+
"\u672C\u5468\u4E8B\u5B9E\uFF08JSON\uFF09\uFF1A",
|
|
628
|
+
JSON.stringify(facts, null, 1)
|
|
629
|
+
].join("\n");
|
|
630
|
+
}
|
|
631
|
+
function renderTemplateReport(facts) {
|
|
632
|
+
const d = (iso) => iso.slice(0, 10);
|
|
633
|
+
const lines = [`\u672C\u5468\uFF08${d(facts.windowStart)} ~ ${d(facts.windowEnd)}\uFF09`];
|
|
634
|
+
lines.push(`\u8868\u8FBE ${facts.spoken} \u6B21\uFF0C\u9759\u9ED8 ${facts.silent} \u6B21\u3002`);
|
|
635
|
+
if (facts.silentTopReasons.length > 0) {
|
|
636
|
+
lines.push(`\u9759\u9ED8\u4E3B\u56E0\uFF1A${facts.silentTopReasons.map((r) => `${r.reason}\uFF08${r.count}\uFF09`).join("\u3001")}\u3002`);
|
|
749
637
|
}
|
|
750
|
-
if (
|
|
751
|
-
|
|
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;
|
|
638
|
+
if (facts.profileAdds.length > 0) {
|
|
639
|
+
lines.push(`\u65B0\u8BA4\u8BC6 ${facts.profileAdds.length} \u6761\uFF1A${facts.profileAdds.map((a) => a.content).join("\uFF1B")}\u3002`);
|
|
759
640
|
}
|
|
760
|
-
if (
|
|
761
|
-
const
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
e.updateCount += 1;
|
|
766
|
-
}
|
|
641
|
+
if (facts.ledger.added.length > 0 || facts.ledger.done.length > 0) {
|
|
642
|
+
const parts = [];
|
|
643
|
+
if (facts.ledger.added.length > 0) parts.push(`\u65B0\u589E\u5F85\u529E\uFF1A${facts.ledger.added.join("\u3001")}`);
|
|
644
|
+
if (facts.ledger.done.length > 0) parts.push(`\u5DF2\u89E3\u51B3\uFF1A${facts.ledger.done.join("\u3001")}`);
|
|
645
|
+
lines.push(`${parts.join("\uFF1B")}\u3002`);
|
|
767
646
|
}
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
}
|
|
647
|
+
if (facts.ledger.stale.length > 0) {
|
|
648
|
+
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
649
|
}
|
|
791
|
-
|
|
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
|
-
};
|
|
650
|
+
if (facts.seeds.added.length > 0) {
|
|
651
|
+
lines.push(`\u7D20\u6750\u6C60\u65B0\u589E ${facts.seeds.added.length} \u6761\uFF0C\u5DF2\u6D88\u8D39 ${facts.seeds.consumed.length} \u6761\u3002`);
|
|
827
652
|
}
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
);
|
|
653
|
+
if (facts.seeds.waiting.length > 0) {
|
|
654
|
+
lines.push(`\u8FD8\u6CA1\u804A\u8FC7\u7684\u7D20\u6750\uFF1A${facts.seeds.waiting.join("\u3001")}\u3002`);
|
|
836
655
|
}
|
|
837
|
-
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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);
|
|
656
|
+
if (facts.reports.total > 0) {
|
|
657
|
+
const reasonPart = facts.reports.reasons.length > 0 ? `\uFF08${facts.reports.reasons.map((r) => `${r.reason} ${r.count} \u6B21`).join("\u3001")}\uFF09` : "";
|
|
658
|
+
lines.push(`\u6295\u9012\u62A5\u8D26 ${facts.reports.total} \u6B21\uFF1A\u7528\u7D20\u6750 ${facts.reports.material}\u3001\u8BF4\u771F\u5FC3\u8BDD ${facts.reports.heartfelt}\u3001\u6CA1\u8BF4\u8BDD ${facts.reports.silent}${reasonPart}\u3002`);
|
|
659
|
+
}
|
|
660
|
+
if (facts.reports.missing > 0) {
|
|
661
|
+
lines.push(`\u53E6\u6709 ${facts.reports.missing} \u6B21\u6295\u9012\u6CA1\u7B49\u5230\u62A5\u8D26\uFF08\u8FD9\u90E8\u5206\u6CA1\u7B97\u8FDB\u504F\u597D\u7EDF\u8BA1\uFF09\u3002`);
|
|
662
|
+
}
|
|
663
|
+
if (facts.peakHours.length > 0) {
|
|
664
|
+
lines.push(`\u6D3B\u8DC3\u9AD8\u5CF0\uFF1A${facts.peakHours.join("\u3001")}\u3002`);
|
|
665
|
+
}
|
|
666
|
+
return lines.join("\n");
|
|
860
667
|
}
|
|
861
668
|
|
|
862
669
|
// src/core/preset-install.ts
|
|
863
|
-
import
|
|
670
|
+
import fs5 from "fs";
|
|
864
671
|
import os from "os";
|
|
865
|
-
import
|
|
672
|
+
import path8 from "path";
|
|
866
673
|
import { fileURLToPath } from "url";
|
|
867
674
|
var COMPOSITION_FILE = "agent.cordis.yml";
|
|
868
675
|
var METADATA_FILE = "preset.yml";
|
|
@@ -870,14 +677,14 @@ var BUNDLED_PRESET_ID = "heartbeat";
|
|
|
870
677
|
function bundledPresetDir(moduleUrl, id = BUNDLED_PRESET_ID) {
|
|
871
678
|
let dir;
|
|
872
679
|
try {
|
|
873
|
-
dir =
|
|
680
|
+
dir = path8.dirname(fileURLToPath(moduleUrl));
|
|
874
681
|
} catch {
|
|
875
682
|
return void 0;
|
|
876
683
|
}
|
|
877
684
|
for (let depth = 0; depth < 5; depth += 1) {
|
|
878
|
-
const candidate =
|
|
879
|
-
if (
|
|
880
|
-
const parent =
|
|
685
|
+
const candidate = path8.join(dir, "assets", "presets", id);
|
|
686
|
+
if (fs5.existsSync(path8.join(candidate, COMPOSITION_FILE))) return candidate;
|
|
687
|
+
const parent = path8.dirname(dir);
|
|
881
688
|
if (parent === dir) break;
|
|
882
689
|
dir = parent;
|
|
883
690
|
}
|
|
@@ -887,12 +694,12 @@ function userPresetRoot(roots) {
|
|
|
887
694
|
const found = roots?.find(
|
|
888
695
|
(root) => root?.trust === "user" && typeof root.path === "string" && root.path.length > 0
|
|
889
696
|
);
|
|
890
|
-
return found?.path === void 0 ? void 0 :
|
|
697
|
+
return found?.path === void 0 ? void 0 : path8.resolve(found.path);
|
|
891
698
|
}
|
|
892
699
|
function conventionalUserPresetRoot(env = process.env, home = os.homedir()) {
|
|
893
700
|
const override = env.DSH_HOME?.trim();
|
|
894
|
-
const root = override && override.length > 0 ? override :
|
|
895
|
-
return
|
|
701
|
+
const root = override && override.length > 0 ? override : path8.join(home, ".dsh");
|
|
702
|
+
return path8.join(root, ".agent-presets");
|
|
896
703
|
}
|
|
897
704
|
function installBundledPreset(options) {
|
|
898
705
|
const id = options.id && options.id.length > 0 ? options.id : BUNDLED_PRESET_ID;
|
|
@@ -924,12 +731,12 @@ function installBundledPreset(options) {
|
|
|
924
731
|
detail: "the roster mounts no user preset root (includeUserRoot=false)"
|
|
925
732
|
};
|
|
926
733
|
}
|
|
927
|
-
const dir =
|
|
928
|
-
const composition =
|
|
734
|
+
const dir = path8.join(root, id);
|
|
735
|
+
const composition = path8.join(dir, COMPOSITION_FILE);
|
|
929
736
|
try {
|
|
930
|
-
if (
|
|
737
|
+
if (fs5.existsSync(composition)) {
|
|
931
738
|
if (options.force !== true) {
|
|
932
|
-
const drifted = !sameBytes(composition,
|
|
739
|
+
const drifted = !sameBytes(composition, path8.join(bundledDir, COMPOSITION_FILE));
|
|
933
740
|
return {
|
|
934
741
|
action: "exists",
|
|
935
742
|
id,
|
|
@@ -938,7 +745,7 @@ function installBundledPreset(options) {
|
|
|
938
745
|
detail: drifted ? "kept as-is (differs from the bundled template)" : "kept as-is"
|
|
939
746
|
};
|
|
940
747
|
}
|
|
941
|
-
|
|
748
|
+
fs5.copyFileSync(path8.join(bundledDir, COMPOSITION_FILE), composition);
|
|
942
749
|
return {
|
|
943
750
|
action: "restored",
|
|
944
751
|
id,
|
|
@@ -947,11 +754,11 @@ function installBundledPreset(options) {
|
|
|
947
754
|
detail: "composition replaced from the bundled template"
|
|
948
755
|
};
|
|
949
756
|
}
|
|
950
|
-
const existed =
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const metadata =
|
|
954
|
-
if (!
|
|
757
|
+
const existed = fs5.existsSync(dir);
|
|
758
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
759
|
+
fs5.copyFileSync(path8.join(bundledDir, COMPOSITION_FILE), composition);
|
|
760
|
+
const metadata = path8.join(dir, METADATA_FILE);
|
|
761
|
+
if (!fs5.existsSync(metadata)) fs5.copyFileSync(path8.join(bundledDir, METADATA_FILE), metadata);
|
|
955
762
|
return {
|
|
956
763
|
action: existed ? "repaired" : "created",
|
|
957
764
|
id,
|
|
@@ -969,21 +776,21 @@ function describeInstall(result) {
|
|
|
969
776
|
return `preset ${result.id} ${result.action}${where}${why}`;
|
|
970
777
|
}
|
|
971
778
|
function presetStatus(moduleUrl, id = BUNDLED_PRESET_ID, root = conventionalUserPresetRoot()) {
|
|
972
|
-
const dir =
|
|
779
|
+
const dir = path8.join(root, id);
|
|
973
780
|
const bundledDir = bundledPresetDir(moduleUrl, id);
|
|
974
|
-
const installed =
|
|
781
|
+
const installed = fs5.existsSync(path8.join(dir, COMPOSITION_FILE));
|
|
975
782
|
return {
|
|
976
783
|
id,
|
|
977
784
|
dir,
|
|
978
785
|
...bundledDir === void 0 ? {} : { bundledDir },
|
|
979
786
|
installed,
|
|
980
|
-
compositionMatches: installed && bundledDir !== void 0 && sameBytes(
|
|
981
|
-
metadataMatches: bundledDir !== void 0 &&
|
|
787
|
+
compositionMatches: installed && bundledDir !== void 0 && sameBytes(path8.join(dir, COMPOSITION_FILE), path8.join(bundledDir, COMPOSITION_FILE)),
|
|
788
|
+
metadataMatches: bundledDir !== void 0 && fs5.existsSync(path8.join(dir, METADATA_FILE)) && sameBytes(path8.join(dir, METADATA_FILE), path8.join(bundledDir, METADATA_FILE))
|
|
982
789
|
};
|
|
983
790
|
}
|
|
984
791
|
function sameBytes(left, right) {
|
|
985
792
|
try {
|
|
986
|
-
return
|
|
793
|
+
return fs5.readFileSync(left).equals(fs5.readFileSync(right));
|
|
987
794
|
} catch {
|
|
988
795
|
return false;
|
|
989
796
|
}
|
|
@@ -991,10 +798,6 @@ function sameBytes(left, right) {
|
|
|
991
798
|
|
|
992
799
|
export {
|
|
993
800
|
createPathGuard,
|
|
994
|
-
atomicWriteJsonSync,
|
|
995
|
-
shredFileSync,
|
|
996
|
-
appendAuditLine,
|
|
997
|
-
pruneAuditFile,
|
|
998
801
|
deepMerge,
|
|
999
802
|
loadPolicy,
|
|
1000
803
|
updateUserPolicy,
|
|
@@ -1004,6 +807,8 @@ export {
|
|
|
1004
807
|
markDone,
|
|
1005
808
|
scanPending,
|
|
1006
809
|
pendingOlderThan,
|
|
810
|
+
preferenceFilePath,
|
|
811
|
+
recordDelivery,
|
|
1007
812
|
loadInterests,
|
|
1008
813
|
loadWatchlist,
|
|
1009
814
|
checkWatchlist,
|
|
@@ -1011,16 +816,17 @@ export {
|
|
|
1011
816
|
completeWander,
|
|
1012
817
|
adviseRefillWander,
|
|
1013
818
|
browseStatus,
|
|
1014
|
-
loadProfileSchema,
|
|
1015
|
-
profileFilePath,
|
|
1016
|
-
loadProfile,
|
|
1017
|
-
applyOpsToDoc,
|
|
1018
|
-
runDeterministicAging,
|
|
1019
|
-
persistWithJournal,
|
|
1020
|
-
verifyProfile,
|
|
1021
|
-
rebuildProfile,
|
|
1022
819
|
ensureRegistered,
|
|
1023
820
|
sendNewMessageHint,
|
|
821
|
+
sendWeeklyReadyHint,
|
|
822
|
+
weeklyDirPath,
|
|
823
|
+
weeklyDue,
|
|
824
|
+
ensureWeeklyAnchor,
|
|
825
|
+
saveWeeklyReport,
|
|
826
|
+
listWeeklyReports,
|
|
827
|
+
readWeeklyReport,
|
|
828
|
+
buildWeeklyPrompt,
|
|
829
|
+
renderTemplateReport,
|
|
1024
830
|
BUNDLED_PRESET_ID,
|
|
1025
831
|
userPresetRoot,
|
|
1026
832
|
conventionalUserPresetRoot,
|
|
@@ -1028,4 +834,4 @@ export {
|
|
|
1028
834
|
describeInstall,
|
|
1029
835
|
presetStatus
|
|
1030
836
|
};
|
|
1031
|
-
//# sourceMappingURL=chunk-
|
|
837
|
+
//# sourceMappingURL=chunk-2SYXKKZF.js.map
|