@kanadego/dsh-heartbeat 1.9.0 → 1.9.2
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 +58 -71
- package/assets/frontwin.ps1 +1 -0
- package/assets/idle.ps1 +1 -0
- package/assets/screenpulse.ps1 +4 -2
- package/config/profile-schema.json +306 -306
- package/cordis.patch.yml +18 -39
- package/dist/{bindings-XPPSKILN.js → bindings-225SI7KC.js} +3 -2
- package/dist/{chunk-63FUJLIF.js → chunk-3MDBU6WY.js} +17 -2
- package/dist/chunk-3MDBU6WY.js.map +1 -0
- package/dist/chunk-AJ5HLAL7.js +2477 -0
- package/dist/chunk-AJ5HLAL7.js.map +1 -0
- package/dist/{chunk-VIZNIQLK.js → chunk-CUOSICYJ.js} +82 -74
- package/dist/chunk-CUOSICYJ.js.map +1 -0
- package/dist/chunk-IG6YMUHW.js +569 -0
- package/dist/chunk-IG6YMUHW.js.map +1 -0
- package/dist/chunk-IV2ZWQA3.js +56 -0
- package/dist/chunk-IV2ZWQA3.js.map +1 -0
- package/dist/{chunk-PQTSXW4F.js → chunk-JDH2LDFS.js} +97 -71
- package/dist/chunk-JDH2LDFS.js.map +1 -0
- package/dist/chunk-K5Y6JP2B.js +399 -0
- package/dist/chunk-K5Y6JP2B.js.map +1 -0
- package/dist/{chunk-RB3NLLGF.js → chunk-KB5SMG3F.js} +3 -3
- package/dist/{chunk-J6ZTRFFW.js → chunk-TY6UP74X.js} +6 -2
- package/dist/chunk-TY6UP74X.js.map +1 -0
- package/dist/chunk-WRUTATW4.js +47 -0
- package/dist/chunk-WRUTATW4.js.map +1 -0
- package/dist/{chunk-IPOZFZPY.js → chunk-XFNCTOW7.js} +48 -10
- package/dist/chunk-XFNCTOW7.js.map +1 -0
- package/dist/chunk-ZLV4NMFX.js +333 -0
- package/dist/chunk-ZLV4NMFX.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +565 -497
- package/dist/cli/index.js.map +1 -1
- package/dist/{inbox-57NHQHY3.js → inbox-P26DDMWC.js} +5 -3
- package/dist/index.d.ts +550 -0
- package/dist/index.js +665 -2730
- package/dist/index.js.map +1 -1
- package/dist/{migrate-N6NXMZ3G.js → migrate-D4VKAKYT.js} +4 -3
- package/dist/orchestrator-EUTZ3KG6.js +62 -0
- package/dist/{pool-7I4VACG2.js → pool-UTXQJJ5C.js} +5 -3
- package/dist/{snapshot-ZBCSX67Y.js → snapshot-K5VHETTK.js} +5 -4
- package/dist/snapshot-K5VHETTK.js.map +1 -0
- package/package.json +83 -76
- package/config/watchlist.json +0 -9
- package/dist/chunk-2SYXKKZF.js +0 -837
- package/dist/chunk-2SYXKKZF.js.map +0 -1
- package/dist/chunk-63FUJLIF.js.map +0 -1
- package/dist/chunk-IFTFDHZX.js +0 -203
- package/dist/chunk-IFTFDHZX.js.map +0 -1
- package/dist/chunk-IPOZFZPY.js.map +0 -1
- package/dist/chunk-J6ZTRFFW.js.map +0 -1
- package/dist/chunk-PQTSXW4F.js.map +0 -1
- package/dist/chunk-VIZNIQLK.js.map +0 -1
- /package/dist/{bindings-XPPSKILN.js.map → bindings-225SI7KC.js.map} +0 -0
- /package/dist/{chunk-RB3NLLGF.js.map → chunk-KB5SMG3F.js.map} +0 -0
- /package/dist/{inbox-57NHQHY3.js.map → inbox-P26DDMWC.js.map} +0 -0
- /package/dist/{migrate-N6NXMZ3G.js.map → migrate-D4VKAKYT.js.map} +0 -0
- /package/dist/{pool-7I4VACG2.js.map → orchestrator-EUTZ3KG6.js.map} +0 -0
- /package/dist/{snapshot-ZBCSX67Y.js.map → pool-UTXQJJ5C.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ledger/ledger.ts","../src/browse/preference.ts","../src/browse/browse.ts","../src/core/time-window.ts","../src/notify/notify.ts","../src/core/ps.ts","../src/weekly/report.ts"],"sourcesContent":["// Ledger (\"账本\") - the ONE shared ledger (design doc §5, requirement 5).\n// Human-readable Markdown by charter; the user may edit it by hand, so the\n// parser is tolerant: unknown lines are preserved verbatim on rewrite.\n\nimport path from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { readText, writeText } from '../vault/vault.js';\n\nconst DAY_MS = 86_400_000;\n\nexport interface LedgerEntry {\n id: string;\n date: string; // \"YYYY-MM-DD\"\n time: string; // \"HH:MM\"\n status: 'open' | 'done';\n text: string;\n}\n\nexport function ledgerFilePath(dataDir: string): string {\n return path.join(dataDir, 'ledger.md');\n}\n\nconst LINE_RE = /^- \\[(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2})\\]\\[(open|done)\\]\\[#([0-9a-f]{6})\\] (.*)$/;\n\nfunction renderEntry(e: LedgerEntry): string {\n return `- [${e.date} ${e.time}][${e.status}][#${e.id}] ${e.text}`;\n}\n\nexport function readLedger(guard: PathGuard, file: string): { header: string; entries: LedgerEntry[]; rawLines: string[] } {\n const raw = readText(guard, file, '# 账本\\n');\n const lines = raw.split('\\n');\n const entries: LedgerEntry[] = [];\n const rawLines: string[] = [];\n for (const line of lines) {\n const m = LINE_RE.exec(line);\n if (m) {\n entries.push({ date: m[1]!, time: m[2]!, status: m[3] as 'open' | 'done', id: m[4]!, text: m[5]! });\n }\n rawLines.push(line);\n }\n return { header: lines[0] ?? '# 账本', entries, rawLines };\n}\n\nexport function appendEntry(guard: PathGuard, file: string, text: string, now = Date.now()): LedgerEntry {\n const textTrimmed = text.trim();\n if (!textTrimmed) throw new Error('ledger entry must not be empty');\n const d = new Date(now);\n const pad = (n: number) => String(n).padStart(2, '0');\n const entry: LedgerEntry = {\n id: randomUUID().slice(0, 6),\n date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,\n time: `${pad(d.getHours())}:${pad(d.getMinutes())}`,\n status: 'open',\n text: textTrimmed.replace(/\\r?\\n/g, ' '),\n };\n const { rawLines } = readLedger(guard, file);\n rawLines.push(renderEntry(entry));\n writeText(guard, file, rawLines.join('\\n').replace(/\\n*$/, '\\n'));\n return entry;\n}\n\n/** Mark an entry done by id (preferred) or unique text substring. The date is\n * restamped to the completion day (time was already) so weekly reporting can\n * count \"resolved this week\" — the creation timestamp lives on in nothing,\n * but done entries are rarely aged against. */\nexport function markDone(guard: PathGuard, file: string, key: string, now = Date.now()): LedgerEntry | null {\n const { rawLines, entries } = readLedger(guard, file);\n const target = entries.find((e) => e.status === 'open' && (e.id === key || e.text.includes(key)));\n if (!target) return null;\n const d = new Date(now);\n const pad = (n: number) => String(n).padStart(2, '0');\n const doneDate = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n const out = rawLines.map((line) => {\n if (line.includes(`#${target.id}] `)) {\n return `- [${doneDate} ${pad(d.getHours())}:${pad(d.getMinutes())}][done][#${target.id}] ${target.text}`;\n }\n return line;\n });\n writeText(guard, file, out.join('\\n').replace(/\\n*$/, '\\n'));\n return target;\n}\n\n/** Open items for the reflection digest (§7.6 账本待办). */\nexport function scanPending(guard: PathGuard, file: string, now = Date.now()): LedgerEntry[] {\n const { entries } = readLedger(guard, file);\n return entries.filter((e) => e.status === 'open').sort((a, b) => (a.date < b.date ? -1 : 1));\n}\n\n/** Open entries older than N days (跟进时机的\"自然到期\"参考,§3.1 四问之一). */\nexport function pendingOlderThan(guard: PathGuard, file: string, days: number, now = Date.now()): LedgerEntry[] {\n const cutoff = new Date(now - days * DAY_MS).toISOString().slice(0, 10);\n return scanPending(guard, file, now).filter((e) => e.date <= cutoff);\n}\n","// Topic preference (2026-10-06): consumption-driven bias for wander focus\n// selection. The persona's seed_report attributions (and the engine-room\n// fallback) fold into per-topic delivered/adopted counters; pickFocus reads\n// the smoothed rate as a weight on top of round-robin recency.\n//\n// Anti-lockdown guarantees (user-reviewed design):\n// - weight clamped to [W_MIN, W_MAX] — a favorite doubles at most, a cold\n// topic never drops below half;\n// - Laplace smoothing — a topic with no data sits near neutral, not zero;\n// - exponential decay (30-day half-life) — old glory fades;\n// - pickFocus additionally enforces a hard starvation floor (14 days).\n//\n// Storage is one JSON file through the vault (DPAPI): topic keys derive from\n// the user's profile/interests, so they are user-adjacent data.\n\nimport path from 'node:path';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { loadJson, saveJson } from '../vault/vault.js';\n\nconst DAY_MS = 86_400_000;\n\nexport const W_MIN = 0.5;\nexport const W_MAX = 2;\n/** Laplace prior: rate = (adopted + A) / (delivered + D). Neutral ≈ A/D. */\nexport const SMOOTH_ADOPTED = 1;\nexport const SMOOTH_DELIVERED = 4;\n/** Counts decay with this half-life (days) since the topic's last activity. */\nexport const DECAY_HALF_LIFE_DAYS = 30;\n\nexport interface TopicPref {\n delivered: number;\n adopted: number;\n lastDeliveredAt: number;\n lastAdoptedAt: number;\n}\n\nexport interface PrefState {\n version: 1;\n topics: Record<string, TopicPref>;\n updatedAt: number;\n}\n\nexport function preferenceFilePath(dataDir: string): string {\n return path.join(dataDir, 'preference.json');\n}\n\n/** Non-negative finite number or 0 (v1.9.0 guard against hand-edited files). */\nfunction num(v: unknown): number {\n const n = Number(v);\n return Number.isFinite(n) && n >= 0 ? n : 0;\n}\n\n/** One stored entry → a trustworthy counter. Returns null for a garbled entry:\n * a NaN delivered/adopted used to survive the load and poison the whole\n * ordering (NaN makes every comparison false, so the topic neither aged nor\n * sorted). Dropping the entry is the safe read — the loop refills it. */\nfunction sanitizeTopicPref(raw: unknown): TopicPref | null {\n if (!raw || typeof raw !== 'object') return null;\n const t = raw as Partial<TopicPref>;\n if (!Number.isFinite(Number(t.delivered)) || !Number.isFinite(Number(t.adopted))) return null;\n const delivered = Math.floor(num(t.delivered));\n return {\n delivered,\n adopted: Math.min(delivered, Math.floor(num(t.adopted))),\n lastDeliveredAt: num(t.lastDeliveredAt),\n lastAdoptedAt: num(t.lastAdoptedAt),\n };\n}\n\nexport function loadPreference(guard: PathGuard, file: string): PrefState {\n const doc = loadJson<Partial<PrefState>>(guard, file);\n if (!doc || typeof doc !== 'object' || !doc.topics || typeof doc.topics !== 'object') {\n return { version: 1, topics: {}, updatedAt: 0 };\n }\n const topics: Record<string, TopicPref> = {};\n for (const [key, raw] of Object.entries(doc.topics)) {\n const t = sanitizeTopicPref(raw);\n if (key && t) topics[key] = t;\n }\n const updatedAt = Number(doc.updatedAt);\n return { version: 1, topics, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };\n}\n\nexport function savePreference(guard: PathGuard, file: string, state: PrefState): void {\n saveJson(guard, file, state);\n}\n\n/** Fold one delivery into the counters. offeredTopics = every topic in the\n * package; adoptedTopics = the credited ones (subset by construction — unknown\n * keys are still tolerated so the two lists can drift safely). */\nexport function recordDelivery(\n guard: PathGuard,\n file: string,\n opts: { offeredTopics: string[]; adoptedTopics: string[]; now: number },\n): PrefState {\n const state = loadPreference(guard, file);\n const adopted = new Set(opts.adoptedTopics);\n for (const topic of opts.offeredTopics) {\n if (!topic) continue;\n const t = state.topics[topic] ?? { delivered: 0, adopted: 0, lastDeliveredAt: 0, lastAdoptedAt: 0 };\n t.delivered += 1;\n t.lastDeliveredAt = opts.now;\n if (adopted.has(topic)) {\n t.adopted += 1;\n t.lastAdoptedAt = opts.now;\n }\n state.topics[topic] = t;\n }\n state.updatedAt = opts.now;\n savePreference(guard, file, state);\n return state;\n}\n\n/** Time-decayed counts (30-day half-life since last activity of each kind). */\nexport function decayed(t: TopicPref, now: number): { delivered: number; adopted: number } {\n const dAgeDays = Math.max(0, now - t.lastDeliveredAt) / DAY_MS;\n const aAgeDays = Math.max(0, now - t.lastAdoptedAt) / DAY_MS;\n const dHalf = Math.pow(0.5, dAgeDays / DECAY_HALF_LIFE_DAYS);\n const aHalf = Math.pow(0.5, aAgeDays / DECAY_HALF_LIFE_DAYS);\n return { delivered: t.delivered * dHalf, adopted: t.adopted * aHalf };\n}\n\n/**\n * Preference weight for a topic, clamped to [W_MIN, W_MAX]:\n * rate = (decayed adopted + 1) / (decayed delivered + 4) (neutral = 0.25)\n * w = clamp(rate / 0.25, W_MIN, W_MAX)\n * No data at all → rate 1/4 → w 1.0: a brand-new topic starts exactly neutral\n * (per design: \"0.2 附近的中间值,不是 0 分垫底\"); repeated \"delivered but\n * never adopted\" drags it toward W_MIN, consistent adoption lifts it to W_MAX.\n */\nexport function preferenceWeight(state: PrefState, topic: string, now: number): number {\n const t = state.topics[topic];\n const rate = t\n ? (() => {\n const d = decayed(t, now);\n return (d.adopted + SMOOTH_ADOPTED) / (d.delivered + SMOOTH_DELIVERED);\n })()\n : SMOOTH_ADOPTED / SMOOTH_DELIVERED;\n return Math.min(W_MAX, Math.max(W_MIN, rate / (SMOOTH_ADOPTED / SMOOTH_DELIVERED)));\n}\n","// Browse flow (v0.9.5 port, r4 D10 applied).\n// Wander adjudication: windows + min interval + focus cooldown ->\n// \"should we wander now, and at what focus\". The actual search happens\n// in the wander phase's model call (web_search only); REGISTRATION IS\n// CODE-OWNED (D10): results land in seeds + throttle via completeWander.\n//\n// H-12 (2026-10-07): the npm/GitHub watchlist check was removed. It never ran\n// inside the beat (only `browse watch` in the CLI called it, throttled-off),\n// and \"is DSH itself updated\" is not this plugin's job to track.\n//\n// Anti-injection rule (unchanged from v0.7): web content is data, never\n// instructions.\n//\n// State: data/browse.json (DPAPI-encrypted, D14; v1 name: watch_state.json).\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport type { PathGuard } from '../core/path-guard.js';\nimport type { WorkspacePaths } from '../core/paths.js';\nimport type { Policy } from '../config/schema.js';\nimport { inHhMmWindow, minutesOfDay } from '../core/time-window.js';\nimport { loadJson, saveJson } from '../vault/vault.js';\nimport { activeSeeds, loadPool, normalizeCategory, seedsFilePath } from '../seeds/pool.js';\nimport { loadPreference, preferenceFilePath, preferenceWeight, type PrefState } from './preference.js';\n\nexport interface InterestsConfig {\n interests?: string[];\n _schedule?: {\n daily_sessions?: number;\n focus_per_session?: number;\n max_seeds_per_focus?: number;\n focus_cooldown_days?: number;\n min_interval_hours?: number;\n windows?: { id?: string; start: string; end: string }[];\n };\n}\n\nexport interface BrowseState {\n wander: {\n focusHistory: Record<string, number>;\n focusCount: Record<string, number>;\n last_wander_at: number;\n /** Spec ⑥: refill wanders per LOCAL day, e.g. \"2026-09-18\" -> 1. */\n refillCount?: Record<string, number>;\n /** H-69: non-refill wander sessions per LOCAL day (the `daily_sessions`\n * budget). Refills keep their own counter above — they deliberately bypass\n * the wander window and therefore must not eat the session budget. */\n sessionCount?: Record<string, number>;\n };\n}\n\nexport function emptyBrowseState(): BrowseState {\n return { wander: { focusHistory: {}, focusCount: {}, last_wander_at: 0, refillCount: {}, sessionCount: {} } };\n}\n\nexport function browseStatePath(paths: WorkspacePaths): string {\n return path.join(paths.dataDir, 'browse.json');\n}\n\nfunction readJsonFile<T>(file: string, fallback: T): T {\n try {\n return JSON.parse(fs.readFileSync(file, 'utf8')) as T;\n } catch {\n return fallback;\n }\n}\n\n/** Factory configs are read-only; a user layer with the same filename replaces them. */\nexport function loadInterests(paths: WorkspacePaths): InterestsConfig {\n const userPath = path.join(paths.settingsDir, 'interests.json');\n if (fs.existsSync(userPath)) return readJsonFile<InterestsConfig>(userPath, { interests: [], _schedule: {} });\n return readJsonFile<InterestsConfig>(path.join(paths.configDir, 'interests.json'), { interests: [], _schedule: {} });\n}\n\n/**\n * H-65: the state file outlives upgrades and is hand-inspectable, so a row\n * missing a field must not throw its way into a `beat_error` on every beat.\n * Garbage values are dropped back to the empty-state defaults.\n */\nexport function normalizeBrowseState(raw: unknown): BrowseState {\n const out = emptyBrowseState();\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;\n const r = raw as Partial<BrowseState>;\n const w = (r.wander && typeof r.wander === 'object' ? r.wander : {}) as Partial<BrowseState['wander']>;\n out.wander.focusHistory = numberRecord(w.focusHistory);\n out.wander.focusCount = numberRecord(w.focusCount);\n out.wander.refillCount = numberRecord(w.refillCount);\n out.wander.sessionCount = numberRecord(w.sessionCount);\n if (typeof w.last_wander_at === 'number' && Number.isFinite(w.last_wander_at)) out.wander.last_wander_at = w.last_wander_at;\n return out;\n}\n\nfunction numberRecord(v: unknown): Record<string, number> {\n const out: Record<string, number> = {};\n if (!v || typeof v !== 'object' || Array.isArray(v)) return out;\n for (const [k, n] of Object.entries(v as Record<string, unknown>)) {\n if (typeof n === 'number' && Number.isFinite(n)) out[k] = n;\n }\n return out;\n}\n\nfunction loadState(guard: PathGuard, paths: WorkspacePaths): BrowseState {\n return normalizeBrowseState(loadJson<unknown>(guard, browseStatePath(paths)));\n}\n\n// ── B. wander adjudication (pure-ish, state injected) ───────────────────\n\nexport interface WanderAdvice {\n focus: string | null;\n query: string | null;\n skipped: string | null;\n /** H-69: focuses this session was allowed to examine (`focus_per_session`);\n * `focus` is the first of them. The phase still runs one model turn per\n * focus, so >1 only widens the candidate list the caller may use. */\n candidates?: string[];\n /** H-69: per-focus seed cap (`max_seeds_per_focus`, defaulting to\n * `policy.browse.maxSeedsPerVisit`). The registration loop reads this. */\n maxSeeds?: number;\n}\n\nexport interface WanderSchedule {\n windows: { start: string; end: string }[];\n minIntervalHours: number;\n maxSeedsPerFocus: number;\n /** Wander sessions per local day; 0 = no daily cap. */\n dailySessions: number;\n focusPerSession: number;\n focusCooldownDays: number;\n}\n\n/**\n * H-69 (2026-10-07): `interests._schedule` is the block the settings card edits\n * and, as the user layer, it REPLACES the factory copy — so it wins wherever it\n * overlaps `policy.browse`, which is the compiled-in default. Every key here\n * has a reader now; `min_interval_hours`, `max_seeds_per_focus` and\n * `daily_sessions` used to be dead wiring whose factory values happened to\n * match policy's, which is exactly why nobody noticed they did nothing.\n */\nexport function resolveSchedule(interests: InterestsConfig, policy: Policy): WanderSchedule {\n const sc = interests._schedule ?? {};\n return {\n windows: sc.windows?.length ? sc.windows : (policy.browse.windows as { start: string; end: string }[]),\n minIntervalHours: sc.min_interval_hours ?? policy.browse.minIntervalHours,\n maxSeedsPerFocus: sc.max_seeds_per_focus ?? policy.browse.maxSeedsPerVisit,\n dailySessions: sc.daily_sessions ?? 0,\n focusPerSession: Math.max(1, sc.focus_per_session ?? 1),\n focusCooldownDays: sc.focus_cooldown_days ?? 3,\n };\n}\n\nexport function inWanderWindow(now: Date, windows: { start: string; end: string }[]): string | null {\n const mins = minutesOfDay(now);\n for (const w of windows) {\n if (inHhMmWindow(w.start, w.end, mins)) return `${w.start}-${w.end}`;\n }\n return null;\n}\n\nfunction onCooldown(state: BrowseState, focus: string, cooldownDays: number, now: number): boolean {\n const last = state.wander.focusHistory[focus] ?? 0;\n return last > now - cooldownDays * 86_400_000;\n}\n\n/**\n * Focus pick (2026-10-06): round-robin recency as the base, preference rate as\n * a multiplier. Ordering key = effective age = realAgeSinceLastUse × weight —\n * a favorite's age \"grows\" faster, so it reaches the pick threshold sooner and\n * is wandered up to W_MAX times as often. Lockdown-proof by construction:\n * - cooldown (above) keeps every topic rotating no matter the weight;\n * - weight lives in [W_MIN, W_MAX] (preference.ts);\n * - hard starvation floor: any topic untouched ≥ STARVATION_DAYS ignores\n * weighting entirely and the oldest real age wins.\n * Without a preference state the behavior is exactly the old pure LRU.\n */\nexport const STARVATION_DAYS = 14;\n\nexport function pickFocus(\n state: BrowseState,\n interests: InterestsConfig,\n now: number,\n pref?: PrefState,\n): string | null {\n const sc = interests._schedule ?? {};\n const cooldown = sc.focus_cooldown_days ?? 3;\n const pool = (interests.interests ?? []).filter((t) => !onCooldown(state, t, cooldown, now));\n if (pool.length === 0) return null;\n if (!pref) {\n pool.sort((a, b) => (state.wander.focusHistory[a] ?? 0) - (state.wander.focusHistory[b] ?? 0));\n return pool[0]!;\n }\n const age = (t: string): number => now - (state.wander.focusHistory[t] ?? 0);\n const starved = pool.filter((t) => age(t) >= STARVATION_DAYS * 86_400_000);\n if (starved.length > 0) {\n starved.sort((a, b) => age(b) - age(a)); // oldest real age wins\n return starved[0]!;\n }\n const effAge = (t: string): number => age(t) * preferenceWeight(pref, t, now);\n pool.sort((a, b) => effAge(b) - effAge(a));\n return pool[0]!;\n}\n\n/** Adjudicate: windows + min interval + focus cooldown -> advice | skipped. */\nexport function adviseWander(\n guard: PathGuard,\n paths: WorkspacePaths,\n policy: Policy,\n now = new Date(),\n): WanderAdvice {\n const state = loadState(guard, paths);\n const interests = loadInterests(paths);\n const sched = resolveSchedule(interests, policy);\n const budget = { maxSeeds: sched.maxSeedsPerFocus };\n const win = inWanderWindow(now, sched.windows);\n if (!win) {\n const hh = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;\n return { focus: null, query: null, skipped: `window(now=${hh})`, ...budget };\n }\n // H-69: `daily_sessions` — same LOCAL-day budget shape as the refill quota.\n const today = localDayKey(now);\n const sessionsToday = state.wander.sessionCount?.[today] ?? 0;\n if (sched.dailySessions > 0 && sessionsToday >= sched.dailySessions) {\n return { focus: null, query: null, skipped: `daily-sessions(${sessionsToday})`, ...budget };\n }\n const minGap = sched.minIntervalHours * 3600_000;\n if (now.getTime() - state.wander.last_wander_at < minGap) {\n return { focus: null, query: null, skipped: 'min-interval', ...budget };\n }\n // H-69: `focus_per_session` candidates, drawn without replacement so one\n // session never offers the same focus twice.\n const pref = loadPreference(guard, preferenceFilePath(paths.dataDir));\n const pool = interests.interests ?? [];\n const candidates: string[] = [];\n for (let i = 0; i < sched.focusPerSession; i++) {\n const rest = pool.filter((t) => !candidates.includes(t));\n if (rest.length === 0) break;\n const picked = pickFocus(state, { ...interests, interests: rest }, now.getTime(), pref);\n if (!picked) break;\n candidates.push(picked);\n }\n if (candidates.length === 0) return { focus: null, query: null, skipped: 'no-focus', ...budget };\n return { focus: candidates[0]!, query: `${candidates[0]!} 2026 最新`, skipped: null, candidates, ...budget };\n}\n\n/**\n * Registration is CODE-OWNED (D10): called by the orchestrator after the\n * wander phase's model call returned selections. Records cooldown/count/\n * throttle timestamps for the focus. `refill` additionally bumps the daily\n * refill counter (spec ⑥).\n */\nexport function completeWander(\n guard: PathGuard,\n paths: WorkspacePaths,\n focus: string,\n now = Date.now(),\n opts: { refill?: boolean } = {},\n): { focus: string; count: number; refillsToday?: number } {\n const state = loadState(guard, paths);\n state.wander.focusHistory[focus] = now;\n state.wander.focusCount[focus] = (state.wander.focusCount[focus] ?? 0) + 1;\n state.wander.last_wander_at = now;\n if (!opts.refill) {\n // H-69: count this session against the `daily_sessions` budget. Refills\n // are their own channel (they deliberately bypass the window and the min\n // interval) and stay on refillCount alone.\n const day = localDayKey(new Date(now));\n state.wander.sessionCount = state.wander.sessionCount ?? {};\n state.wander.sessionCount[day] = (state.wander.sessionCount[day] ?? 0) + 1;\n }\n let refillsToday: number | undefined;\n if (opts.refill) {\n const today = localDayKey(new Date(now));\n state.wander.refillCount = state.wander.refillCount ?? {};\n state.wander.refillCount[today] = (state.wander.refillCount[today] ?? 0) + 1;\n refillsToday = state.wander.refillCount[today];\n }\n saveJson(guard, browseStatePath(paths), state);\n return { focus, count: state.wander.focusCount[focus]!, ...(refillsToday === undefined ? {} : { refillsToday }) };\n}\n\n/** LOCAL day key (refill quota is a daily human-day budget, not a UTC day). */\nfunction localDayKey(d: Date): string {\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n return `${y}-${m}-${day}`;\n}\n\n// ── C. refill wander (spec ⑥, 2026-09-18): top up topic stock when it runs\n// dry. Independent of the window/minInterval gates (user decision: may stack\n// with a normal wander in the same beat) but still respects the 3-day focus\n// cooldown, and is capped at REFILL_MAX_PER_DAY per local day.\n\nexport const REFILL_TOPIC_THRESHOLD = 4;\nexport const REFILL_MAX_PER_DAY = 2;\n\nexport interface RefillAdvice {\n focus: string | null;\n query: string | null;\n skipped: string | null;\n topicCount: number;\n refillsToday: number;\n}\n\nexport function adviseRefillWander(\n guard: PathGuard,\n paths: WorkspacePaths,\n policy: Policy,\n now = new Date(),\n): RefillAdvice {\n const state = loadState(guard, paths);\n const today = localDayKey(now);\n const refillsToday = state.wander.refillCount?.[today] ?? 0;\n const topicCount = activeSeeds(loadPool(guard, seedsFilePath(paths.dataDir)))\n .filter((s) => normalizeCategory(s.category) === 'topic').length;\n if (refillsToday >= REFILL_MAX_PER_DAY) {\n return { focus: null, query: null, skipped: `refill-daily-cap(${refillsToday})`, topicCount, refillsToday };\n }\n if (topicCount > REFILL_TOPIC_THRESHOLD) {\n return { focus: null, query: null, skipped: `topic-stock-ok(${topicCount})`, topicCount, refillsToday };\n }\n const interests = loadInterests(paths);\n const focus = pickFocus(state, interests, now.getTime(), loadPreference(guard, preferenceFilePath(paths.dataDir))); // 3-day cooldown still applies\n if (!focus) {\n return { focus: null, query: null, skipped: 'no-focus', topicCount, refillsToday };\n }\n return { focus, query: `${focus} 2026 最新`, skipped: null, topicCount, refillsToday };\n}\n\nexport function browseStatus(guard: PathGuard, paths: WorkspacePaths): BrowseState {\n return loadState(guard, paths);\n}\n","// Shared HH:MM window arithmetic.\n//\n// H-14: quiet hours (`gate.ts`) and wander windows (`browse.ts`) each carried\n// their own copy of this comparison, and only ONE of them handled a window\n// that crosses midnight — so a `22:00-02:00` wander window silently never\n// matched. One implementation, one set of semantics, no drift.\n//\n// Interval is left-closed / right-open, so `22:00-02:00` and `02:00-06:00`\n// never overlap and every minute belongs to at most one window.\n\nconst HH_MM = /^(\\d{1,2}):(\\d{2})$/;\n\n/** \"22:30\" -> 1350; null when malformed (callers treat that as \"never matches\"). */\nexport function parseHhMm(text: string): number | null {\n const m = HH_MM.exec(String(text).trim());\n if (!m) return null;\n const h = Number(m[1]);\n const min = Number(m[2]);\n if (!Number.isFinite(h) || !Number.isFinite(min) || h > 23 || min > 59) return null;\n return h * 60 + min;\n}\n\nexport function minutesOfDay(now: Date): number {\n return now.getHours() * 60 + now.getMinutes();\n}\n\n/** TRUE when `mins` (minutes since local midnight) falls inside [start, end). */\nexport function inHhMmWindow(start: string, end: string, mins: number): boolean {\n const s = parseHhMm(start);\n const e = parseHhMm(end);\n if (s === null || e === null) return false;\n if (s === e) return false; // zero-length window matches nothing\n return s > e ? mins >= s || mins < e : mins >= s && mins < e;\n}\n","// Windows toast channel (D12): attention hint ONLY - never carries the\n// expression body. Wraps assets/notify.ps1 (self-registering AUMID).\n\nimport path from 'node:path';\nimport { runPowerShellFile } from '../core/ps.js';\nimport type { WorkspacePaths } from '../core/paths.js';\n\nasync function runNotify(paths: WorkspacePaths, args: string[]): Promise<{ status: number; out: string }> {\n const r = await runPowerShellFile(path.join(paths.assetsDir, 'notify.ps1'), args, 20_000);\n return { status: r.status, out: `${r.stdout}${r.stderr}`.trim() };\n}\n\n/** Register the toast identity if missing (idempotent, per machine/user). */\nexport async function ensureRegistered(paths: WorkspacePaths): Promise<boolean> {\n const check = await runNotify(paths, ['-Check']);\n if (/REGISTERED: yes/.test(check.out)) return true;\n const reg = await runNotify(paths, ['-RegisterOnly']);\n return reg.status === 0;\n}\n\n/**\n * Fire the \"new message\" hint. Fixed neutral text per D12 - the actual\n * expression lives in the dedicated heartbeat session, never in the toast.\n */\nexport async function sendNewMessageHint(paths: WorkspacePaths): Promise<boolean> {\n const r = await runNotify(paths, ['-Title', 'Heartbeat', '-Message', '有新消息']);\n return r.status === 0 && /TOAST_SENT/.test(r.out);\n}\n\n/** Weekly report ready hint (v1.8.0): same D12 discipline — announces that a\n * report exists, never carries its content. */\nexport async function sendWeeklyReadyHint(paths: WorkspacePaths): Promise<boolean> {\n const r = await runNotify(paths, ['-Title', '心跳周报', '-Message', '本期周报已生成']);\n return r.status === 0 && /TOAST_SENT/.test(r.out);\n}\n","/**\n * H-10: asynchronous PowerShell runner.\n *\n * The probes (idle seconds, front window, screen capture, toasts) used\n * `spawnSync`, which blocks the host's event loop for as long as the script\n * runs — 10s to 45s on a slow machine, and the GUI stalls with it. Same\n * arguments, same timeouts, same \"never throw, degrade instead\" contract, but\n * the loop stays free while the child runs.\n */\nimport { execFile } from 'node:child_process';\n\nexport interface PsResult {\n status: number;\n stdout: string;\n stderr: string;\n}\n\n/** Run `powershell.exe` and resolve with its exit code and streams. Never\n * rejects: a spawn failure and a non-zero exit both come back as `status !== 0`\n * so callers keep their existing \"probe failed\" paths. */\nexport function runPowerShell(args: string[], timeoutMs: number): Promise<PsResult> {\n return new Promise((resolve) => {\n const child = execFile(\n 'powershell.exe',\n ['-NoProfile', '-ExecutionPolicy', 'Bypass', ...args],\n { timeout: timeoutMs, encoding: 'utf8', windowsHide: true },\n (err, stdout, stderr) => {\n const code = (err as { code?: unknown } | null)?.code;\n const status = err ? (typeof code === 'number' ? code : 1) : 0;\n resolve({ status, stdout: String(stdout ?? ''), stderr: String(stderr ?? '') });\n },\n );\n child.on('error', () => { /* surfaced through the callback above */ });\n });\n}\n\n/** Convenience for the `-File <script>` shape used by every asset script. */\nexport function runPowerShellFile(script: string, args: string[], timeoutMs: number): Promise<PsResult> {\n return runPowerShell(['-File', script, ...args], timeoutMs);\n}\n","// Weekly report store + writer helpers (v1.8.0). Deliberately host-free:\r\n// orchestration (when to run, which agent, toast) lives in the orchestrator —\r\n// same split as profile/consolidate.ts. Reports are DPAPI-encrypted because a\r\n// report contains user-identifying content (profile adds, ledger texts) —\r\n// same划线标准 as profile/seeds (design doc §10.5).\r\n\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { atomicWriteJsonSync } from '../core/atomic-fs.js';\r\nimport { loadEncryptedText, saveEncryptedText } from '../vault/vault.js';\r\nimport type { PathGuard } from '../core/path-guard.js';\r\nimport { workspace } from '../core/paths.js';\r\nimport { appendAuditLine } from '../core/audit-log.js';\r\nimport type { WeeklyFacts } from './collect.js';\r\n\r\nexport const WEEKLY_INTERVAL_MS = 7 * 86_400_000;\r\n\r\nexport interface WeeklyReport {\r\n start: string;\r\n end: string;\r\n generatedAt: string;\r\n /** 'llm' = engine-room prose; 'template' = deterministic fallback. */\r\n source: 'llm' | 'template';\r\n text: string;\r\n}\r\n\r\nexport function weeklyDirPath(dataDir: string): string {\r\n return path.join(dataDir, 'weekly');\r\n}\r\n\r\nfunction stateFilePath(dataDir: string): string {\r\n return path.join(weeklyDirPath(dataDir), 'state.json');\r\n}\r\n\r\nfunction reportFileName(endIso: string): string {\r\n return `report-${endIso.slice(0, 10)}.json`;\r\n}\r\n\r\n/**\r\n * Last generation time from the weekly state file; 0 = never anchored.\r\n *\r\n * H-54: \"missing\" and \"present but unparseable\" are NOT the same thing. The\r\n * old code folded both into 0, so `ensureWeeklyAnchor` \"repaired\" a corrupt\r\n * file by stamping NOW — silently, and at the cost of pushing the next report\r\n * a full interval out. A corrupt anchor now keeps its file, writes an audit\r\n * line, and falls back to the file's mtime so the schedule keeps its place.\r\n */\r\nexport function lastWeeklyGeneratedAt(guard: PathGuard, dataDir: string): number {\r\n const file = stateFilePath(dataDir);\r\n let raw: string;\r\n try {\r\n raw = fs.readFileSync(guard.assert(file), 'utf8');\r\n } catch {\r\n return 0; // absent: a fresh install, legitimately never anchored\r\n }\r\n try {\r\n const parsed = JSON.parse(raw) as { lastGeneratedAt?: number };\r\n const n = Number(parsed.lastGeneratedAt);\r\n if (Number.isFinite(n) && n > 0) return n;\r\n } catch {\r\n /* unparseable — handled by the fallback below */\r\n }\r\n try {\r\n appendAuditLine(path.join(workspace().logsDir, 'heartbeat.jsonl'), {\r\n event: 'weekly_anchor_corrupt',\r\n file: path.basename(file),\r\n bytes: Buffer.byteLength(raw),\r\n });\r\n } catch {\r\n /* audit is diagnostic; never let it break the schedule */\r\n }\r\n try {\r\n return fs.statSync(file).mtimeMs;\r\n } catch {\r\n return 0;\r\n }\r\n}\r\n\r\n/** Due when the last report is ≥7 days old. A never-anchored store lands on\r\n * the install-time anchor (see ensureWeeklyAnchor), so a fresh install is NOT\r\n * due — the first report arrives one full interval after installation. */\r\nexport function weeklyDue(guard: PathGuard, dataDir: string, now = Date.now()): boolean {\r\n const last = lastWeeklyGeneratedAt(guard, dataDir);\r\n return now - last >= WEEKLY_INTERVAL_MS;\r\n}\r\n\r\n/** Stamp the install-time anchor for a store that has never generated a report\r\n * (v1.9.0). Returns true when it wrote — the caller audits that. Without this,\r\n * `lastGeneratedAt = 0` made the very first maintenance beat due, so a fresh\r\n * install produced an \"onboarding\" report immediately, contradicting the\r\n * release notes (\"first report after a week\"). */\r\nexport function ensureWeeklyAnchor(guard: PathGuard, dataDir: string, now = Date.now()): boolean {\r\n if (lastWeeklyGeneratedAt(guard, dataDir) > 0) return false;\r\n fs.mkdirSync(weeklyDirPath(dataDir), { recursive: true });\r\n atomicWriteJsonSync(stateFilePath(dataDir), { lastGeneratedAt: now });\r\n return true;\r\n}\r\n\r\n/** Persist a report (DPAPI) and stamp the state file. */\r\nexport function saveWeeklyReport(guard: PathGuard, dataDir: string, report: WeeklyReport): string {\r\n const dir = weeklyDirPath(dataDir);\r\n fs.mkdirSync(dir, { recursive: true });\r\n const file = path.join(dir, reportFileName(report.end));\r\n saveEncryptedText(guard, file, JSON.stringify(report, null, 1));\r\n atomicWriteJsonSync(stateFilePath(dataDir), { lastGeneratedAt: Date.parse(report.generatedAt) });\r\n return file;\r\n}\r\n\r\nexport interface WeeklyReportMeta {\r\n file: string;\r\n start: string;\r\n end: string;\r\n generatedAt: string;\r\n source: string;\r\n}\r\n\r\n/** Newest-first listing for the card; unparsable entries are skipped. */\r\nexport function listWeeklyReports(guard: PathGuard, dataDir: string): WeeklyReportMeta[] {\r\n const out: WeeklyReportMeta[] = [];\r\n try {\r\n for (const file of fs.readdirSync(weeklyDirPath(dataDir))) {\r\n if (!/^report-\\d{4}-\\d{2}-\\d{2}\\.json$/.test(file)) continue;\r\n const rep = readWeeklyReport(guard, dataDir, file);\r\n if (!rep) continue;\r\n out.push({ file, start: rep.start, end: rep.end, generatedAt: rep.generatedAt, source: rep.source });\r\n }\r\n } catch { /* no weekly dir yet */ }\r\n return out.sort((a, b) => (a.end < b.end ? 1 : -1));\r\n}\r\n\r\nexport function readWeeklyReport(guard: PathGuard, dataDir: string, file: string): WeeklyReport | null {\r\n if (!/^report-\\d{4}-\\d{2}-\\d{2}\\.json$/.test(file)) return null; // guard.assert would reject ../ anyway\r\n try {\r\n const raw = loadEncryptedText(guard, path.join(weeklyDirPath(dataDir), file));\r\n if (!raw) return null;\r\n const rep = JSON.parse(raw) as WeeklyReport;\r\n if (typeof rep.text !== 'string') return null;\r\n return rep;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** The engine-room prompt: neutral system narrator, fact-bound, ≤250 chars. */\r\nexport function buildWeeklyPrompt(facts: WeeklyFacts): string {\r\n return [\r\n '你是心跳插件的后台报告器。根据下面的本周事实清单,用中文写一份周报正文。',\r\n '规则:',\r\n '- 叙述者自称「心跳」——这是插件后台,不是任何会话里的 agent;绝不能以会话 agent 的身份或人格自称。',\r\n '- 只报告事实清单里有的内容;某一节没有数据就整节跳过,不要编造、不要凑数。',\r\n '- 语气平实中性,不用感叹号;篇幅不超过 250 字。',\r\n '- 待办积压那一节用商量的语气提一句,不催促。',\r\n '- 直接输出周报正文,不要输出解释、标题或 JSON。',\r\n '',\r\n '本周事实(JSON):',\r\n JSON.stringify(facts, null, 1),\r\n ].join('\\n');\r\n}\r\n\r\n/** Deterministic fallback: the facts, plainly stated. Used when the LLM turn\r\n * fails or comes back empty — a plain report beats no report. */\r\nexport function renderTemplateReport(facts: WeeklyFacts): string {\r\n const d = (iso: string): string => iso.slice(0, 10);\r\n const lines: string[] = [`本周(${d(facts.windowStart)} ~ ${d(facts.windowEnd)})`];\r\n lines.push(`表达 ${facts.spoken} 次,静默 ${facts.silent} 次。`);\r\n if (facts.silentTopReasons.length > 0) {\r\n lines.push(`静默主因:${facts.silentTopReasons.map((r) => `${r.reason}(${r.count})`).join('、')}。`);\r\n }\r\n if (facts.profileAdds.length > 0) {\r\n lines.push(`新认识 ${facts.profileAdds.length} 条:${facts.profileAdds.map((a) => a.content).join(';')}。`);\r\n }\r\n if (facts.ledger.added.length > 0 || facts.ledger.done.length > 0) {\r\n const parts: string[] = [];\r\n if (facts.ledger.added.length > 0) parts.push(`新增待办:${facts.ledger.added.join('、')}`);\r\n if (facts.ledger.done.length > 0) parts.push(`已解决:${facts.ledger.done.join('、')}`);\r\n lines.push(`${parts.join(';')}。`);\r\n }\r\n if (facts.ledger.stale.length > 0) {\r\n lines.push(`挂了很久:${facts.ledger.stale.map((s) => `${s.text}(${s.days} 天)`).join('、')}——要删掉还是继续挂着?`);\r\n }\r\n if (facts.seeds.added.length > 0) {\r\n lines.push(`素材池新增 ${facts.seeds.added.length} 条,已消费 ${facts.seeds.consumed.length} 条。`);\r\n }\r\n if (facts.seeds.waiting.length > 0) {\r\n lines.push(`还没聊过的素材:${facts.seeds.waiting.join('、')}。`);\r\n }\r\n if (facts.reports.total > 0) {\r\n const reasonPart = facts.reports.reasons.length > 0\r\n ? `(${facts.reports.reasons.map((r) => `${r.reason} ${r.count} 次`).join('、')})`\r\n : '';\r\n lines.push(`投递报账 ${facts.reports.total} 次:用素材 ${facts.reports.material}、说真心话 ${facts.reports.heartfelt}、没说话 ${facts.reports.silent}${reasonPart}。`);\r\n }\r\n // v1.9.0: unaccounted deliveries are reported even when no report landed at\r\n // all — a silent rise in \"no report\" is exactly what would otherwise hide.\r\n if (facts.reports.missing > 0) {\r\n lines.push(`另有 ${facts.reports.missing} 次投递没等到报账(这部分没算进偏好统计)。`);\r\n }\r\n if (facts.peakHours.length > 0) {\r\n lines.push(`活跃高峰:${facts.peakHours.join('、')}。`);\r\n }\r\n return lines.join('\\n');\r\n}\r\n"],"mappings":"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","names":["path","DAY_MS","path","path","path","path","path","fs","path","path","fs"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/core/file-lock.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
var STALE_MS = 15e3;
|
|
4
|
+
var WAIT_MS = 3e3;
|
|
5
|
+
var POLL_MS = 25;
|
|
6
|
+
function sleepSync(ms) {
|
|
7
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
8
|
+
}
|
|
9
|
+
function tryAcquire(lock) {
|
|
10
|
+
try {
|
|
11
|
+
const fd = fs.openSync(lock, "wx");
|
|
12
|
+
fs.writeSync(fd, `${process.pid} ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
13
|
+
`);
|
|
14
|
+
fs.closeSync(fd);
|
|
15
|
+
return true;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
if (e.code !== "EEXIST") throw e;
|
|
18
|
+
try {
|
|
19
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > STALE_MS) {
|
|
20
|
+
fs.rmSync(lock, { force: true });
|
|
21
|
+
return tryAcquire(lock);
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
return tryAcquire(lock);
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function withFileLock(target, fn) {
|
|
30
|
+
const lock = `${target}.lock`;
|
|
31
|
+
const deadline = Date.now() + WAIT_MS;
|
|
32
|
+
let held = false;
|
|
33
|
+
for (; ; ) {
|
|
34
|
+
if (tryAcquire(lock)) {
|
|
35
|
+
held = true;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
if (Date.now() >= deadline) break;
|
|
39
|
+
sleepSync(POLL_MS);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return fn();
|
|
43
|
+
} finally {
|
|
44
|
+
if (held) {
|
|
45
|
+
try {
|
|
46
|
+
fs.rmSync(lock, { force: true });
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
withFileLock
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=chunk-IV2ZWQA3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/file-lock.ts"],"sourcesContent":["// Cross-process advisory file lock (H-20, minimal tier).\n//\n// The seed pool and the observation inbox are WHOLE-FILE rewrites of DPAPI\n// ciphertext — the vault cannot append to them — and the CLI runs as a second\n// process against the same files. Two interleaved load→mutate→save cycles\n// therefore let one side silently overwrite the other's write. The full fix\n// (append-only journal + compaction) is v2.0 work; this keeps the writers\n// honest in the meantime.\n//\n// The lock itself is a sibling file created with O_EXCL. A lock older than\n// STALE_MS is treated as abandoned (its holder died mid-write) and reclaimed.\n\nimport fs from 'node:fs';\n\nconst STALE_MS = 15_000;\nconst WAIT_MS = 3_000;\nconst POLL_MS = 25;\n\nfunction sleepSync(ms: number): void {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\nfunction tryAcquire(lock: string): boolean {\n try {\n const fd = fs.openSync(lock, 'wx');\n fs.writeSync(fd, `${process.pid} ${new Date().toISOString()}\\n`);\n fs.closeSync(fd);\n return true;\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;\n // Reclaim a lock whose holder is gone.\n try {\n if (Date.now() - fs.statSync(lock).mtimeMs > STALE_MS) {\n fs.rmSync(lock, { force: true });\n return tryAcquire(lock);\n }\n } catch {\n return tryAcquire(lock); // vanished between open and stat\n }\n return false;\n }\n}\n\n/**\n * Run `fn` while holding the lock for `target`. Acquisition waits up to\n * WAIT_MS, then gives up and runs anyway: a heartbeat must not be stopped by a\n * stale lock, and the caller's own read-modify-write is still the last writer\n * in the common (uncontended) case.\n */\nexport function withFileLock<T>(target: string, fn: () => T): T {\n const lock = `${target}.lock`;\n const deadline = Date.now() + WAIT_MS;\n let held = false;\n for (;;) {\n if (tryAcquire(lock)) {\n held = true;\n break;\n }\n if (Date.now() >= deadline) break;\n sleepSync(POLL_MS);\n }\n try {\n return fn();\n } finally {\n if (held) {\n try {\n fs.rmSync(lock, { force: true });\n } catch { /* already gone */ }\n }\n }\n}\n"],"mappings":";AAYA,OAAO,QAAQ;AAEf,IAAM,WAAW;AACjB,IAAM,UAAU;AAChB,IAAM,UAAU;AAEhB,SAAS,UAAU,IAAkB;AACnC,UAAQ,KAAK,IAAI,WAAW,IAAI,kBAAkB,CAAC,CAAC,GAAG,GAAG,GAAG,EAAE;AACjE;AAEA,SAAS,WAAW,MAAuB;AACzC,MAAI;AACF,UAAM,KAAK,GAAG,SAAS,MAAM,IAAI;AACjC,OAAG,UAAU,IAAI,GAAG,QAAQ,GAAG,KAAI,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,CAAI;AAC/D,OAAG,UAAU,EAAE;AACf,WAAO;AAAA,EACT,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,SAAU,OAAM;AAE1D,QAAI;AACF,UAAI,KAAK,IAAI,IAAI,GAAG,SAAS,IAAI,EAAE,UAAU,UAAU;AACrD,WAAG,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC;AAC/B,eAAO,WAAW,IAAI;AAAA,MACxB;AAAA,IACF,QAAQ;AACN,aAAO,WAAW,IAAI;AAAA,IACxB;AACA,WAAO;AAAA,EACT;AACF;AAQO,SAAS,aAAgB,QAAgB,IAAgB;AAC9D,QAAM,OAAO,GAAG,MAAM;AACtB,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,MAAI,OAAO;AACX,aAAS;AACP,QAAI,WAAW,IAAI,GAAG;AACpB,aAAO;AACP;AAAA,IACF;AACA,QAAI,KAAK,IAAI,KAAK,SAAU;AAC5B,cAAU,OAAO;AAAA,EACnB;AACA,MAAI;AACF,WAAO,GAAG;AAAA,EACZ,UAAE;AACA,QAAI,MAAM;AACR,UAAI;AACF,WAAG,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC;AAAA,MACjC,QAAQ;AAAA,MAAqB;AAAA,IAC/B;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withFileLock
|
|
3
|
+
} from "./chunk-IV2ZWQA3.js";
|
|
1
4
|
import {
|
|
2
5
|
loadEncryptedText,
|
|
3
6
|
saveEncryptedText
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-K5Y6JP2B.js";
|
|
5
8
|
|
|
6
9
|
// src/seeds/pool.ts
|
|
7
10
|
import path from "path";
|
|
@@ -69,6 +72,9 @@ function savePool(guard, file, db) {
|
|
|
69
72
|
const body = db.seeds.map((s) => JSON.stringify(s)).join("\n");
|
|
70
73
|
saveEncryptedText(guard, file, body ? body + "\n" : "");
|
|
71
74
|
}
|
|
75
|
+
function update(guard, file, fn) {
|
|
76
|
+
return withFileLock(file, () => fn(loadPool(guard, file)));
|
|
77
|
+
}
|
|
72
78
|
var activeSeeds = (db) => db.seeds.filter((s) => s.status === "active");
|
|
73
79
|
var archivedSeeds = (db) => db.seeds.filter((s) => s.status === "archived");
|
|
74
80
|
function evictionScore(seed, policy, now) {
|
|
@@ -103,7 +109,14 @@ function archiveSeed(seed, reason, now) {
|
|
|
103
109
|
function retireLimit(s, policy) {
|
|
104
110
|
return normalizeCategory(s.category) === "chat" ? 1 : policy.seeds.retireAfterUsed;
|
|
105
111
|
}
|
|
112
|
+
function isConsumed(s, policy, prevUsedAt, sinceEvidence) {
|
|
113
|
+
if (s.used < retireLimit(s, policy)) return false;
|
|
114
|
+
return !(prevUsedAt > 0 && sinceEvidence > prevUsedAt);
|
|
115
|
+
}
|
|
106
116
|
function addSeed(guard, file, policy, input, now = Date.now(), audit = NO_AUDIT) {
|
|
117
|
+
return withFileLock(file, () => addSeedLocked(guard, file, policy, input, now, audit));
|
|
118
|
+
}
|
|
119
|
+
function addSeedLocked(guard, file, policy, input, now, audit) {
|
|
107
120
|
const db = loadPool(guard, file);
|
|
108
121
|
const text = input.text.trim();
|
|
109
122
|
if (!text) throw new Error("seed text must not be empty");
|
|
@@ -189,84 +202,97 @@ function addSeed(guard, file, policy, input, now = Date.now(), audit = NO_AUDIT)
|
|
|
189
202
|
return { kind: "added", seed, evicted };
|
|
190
203
|
}
|
|
191
204
|
function gcPool(guard, file, policy, now = Date.now(), audit = NO_AUDIT) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
return update(guard, file, (db) => {
|
|
206
|
+
const report = { consumed: 0, expired: 0, coldBench: 0, activeAfter: 0, archiveTrimmed: 0 };
|
|
207
|
+
for (const s of activeSeeds(db)) {
|
|
208
|
+
const ageMs = now - parseIso(s.bornAt);
|
|
209
|
+
const sinceEvidence = parseIso(s.lastEvidenceAt);
|
|
210
|
+
const sinceUsed = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;
|
|
211
|
+
if (isConsumed(s, policy, sinceUsed, sinceEvidence)) {
|
|
212
|
+
archiveSeed(s, "consumed", now);
|
|
213
|
+
report.consumed += 1;
|
|
214
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "consumed", via: "gc", used: s.used });
|
|
215
|
+
} else if (now > parseIso(s.expiresAt)) {
|
|
216
|
+
archiveSeed(s, "expired", now);
|
|
217
|
+
report.expired += 1;
|
|
218
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "expired", via: "gc", used: s.used });
|
|
219
|
+
} else if (s.used === 0 && ageMs >= policy.seeds.coldBenchDays * DAY_MS) {
|
|
220
|
+
archiveSeed(s, "cold_bench", now);
|
|
221
|
+
report.coldBench += 1;
|
|
222
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "cold_bench", via: "gc", used: s.used });
|
|
223
|
+
}
|
|
210
224
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
const archived = db.seeds.filter((s) => s.status === "archived");
|
|
226
|
+
if (archived.length > policy.seeds.archiveCap) {
|
|
227
|
+
const drop = [...archived].sort((a, b) => parseIso(a.retiredAt ?? "") - parseIso(b.retiredAt ?? "")).slice(0, archived.length - policy.seeds.archiveCap);
|
|
228
|
+
const dropped = new Set(drop.map((s) => s.id));
|
|
229
|
+
db.seeds = db.seeds.filter((s) => !dropped.has(s.id));
|
|
230
|
+
report.archiveTrimmed = drop.length;
|
|
231
|
+
emit(audit, { event: "seed_archive_trimmed", count: drop.length, ids: drop.slice(0, 50).map((s) => s.id) });
|
|
232
|
+
}
|
|
233
|
+
report.activeAfter = activeSeeds(db).length;
|
|
234
|
+
savePool(guard, file, db);
|
|
235
|
+
return report;
|
|
236
|
+
});
|
|
223
237
|
}
|
|
224
238
|
function surfaceSeed(guard, file, policy, id, now = Date.now(), audit = NO_AUDIT) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
239
|
+
return update(guard, file, (db) => {
|
|
240
|
+
const s = db.seeds.find((x) => x.id === id && x.status === "active");
|
|
241
|
+
if (!s) return null;
|
|
242
|
+
const prevUsedAt = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;
|
|
243
|
+
s.used += 1;
|
|
244
|
+
s.lastUsedAt = new Date(now).toISOString();
|
|
245
|
+
if (isConsumed(s, policy, prevUsedAt, parseIso(s.lastEvidenceAt))) {
|
|
246
|
+
archiveSeed(s, "consumed", now);
|
|
247
|
+
emit(audit, { event: "seed_retired", id: s.id, reason: "consumed", via: "surface", used: s.used });
|
|
248
|
+
}
|
|
249
|
+
savePool(guard, file, db);
|
|
250
|
+
return s;
|
|
251
|
+
});
|
|
236
252
|
}
|
|
237
253
|
function archiveSeedById(guard, file, id, reason = "completed", now = Date.now(), audit = NO_AUDIT) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
254
|
+
return update(guard, file, (db) => {
|
|
255
|
+
const s = db.seeds.find((x) => x.id === id && x.status === "active");
|
|
256
|
+
if (!s) return null;
|
|
257
|
+
archiveSeed(s, reason, now);
|
|
258
|
+
emit(audit, { event: "seed_retired", id: s.id, reason, via: "manual" });
|
|
259
|
+
savePool(guard, file, db);
|
|
260
|
+
return s;
|
|
261
|
+
});
|
|
245
262
|
}
|
|
246
263
|
function restoreSeed(guard, file, policy, id, now = Date.now(), audit = NO_AUDIT) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
264
|
+
return update(guard, file, (db) => {
|
|
265
|
+
const s = db.seeds.find((x) => x.id === id && x.status === "archived");
|
|
266
|
+
if (!s) return { ok: false, reason: "archived seed not found" };
|
|
267
|
+
const category = normalizeCategory(s.category);
|
|
268
|
+
const catCap = SEED_CATEGORY_CAPS[category];
|
|
269
|
+
const inCategory = activeSeeds(db).filter((x) => normalizeCategory(x.category) === category).length;
|
|
270
|
+
if (inCategory >= catCap) {
|
|
271
|
+
return { ok: false, reason: `category full (${category}: ${catCap}); archive something in it first` };
|
|
272
|
+
}
|
|
273
|
+
if (activeSeeds(db).length >= policy.seeds.maxActive) {
|
|
274
|
+
return { ok: false, reason: `pool full (${policy.seeds.maxActive}); archive something first` };
|
|
275
|
+
}
|
|
276
|
+
s.status = "active";
|
|
277
|
+
s.retireReason = void 0;
|
|
278
|
+
s.retiredAt = void 0;
|
|
279
|
+
s.bornAt = new Date(now).toISOString();
|
|
280
|
+
s.expiresAt = new Date(now + (policy.seeds.ttlDays[s.tag] || 14) * DAY_MS).toISOString();
|
|
281
|
+
s.lastEvidenceAt = new Date(now).toISOString();
|
|
282
|
+
emit(audit, { event: "seed_restored", id: s.id });
|
|
283
|
+
savePool(guard, file, db);
|
|
284
|
+
return { ok: true, seed: s };
|
|
285
|
+
});
|
|
261
286
|
}
|
|
262
287
|
function deleteSeed(guard, file, id, audit = NO_AUDIT) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
288
|
+
return update(guard, file, (db) => {
|
|
289
|
+
const before = db.seeds.length;
|
|
290
|
+
db.seeds = db.seeds.filter((x) => x.id !== id);
|
|
291
|
+
if (db.seeds.length === before) return false;
|
|
292
|
+
emit(audit, { event: "seed_deleted", id });
|
|
293
|
+
savePool(guard, file, db);
|
|
294
|
+
return true;
|
|
295
|
+
});
|
|
270
296
|
}
|
|
271
297
|
|
|
272
298
|
export {
|
|
@@ -288,4 +314,4 @@ export {
|
|
|
288
314
|
restoreSeed,
|
|
289
315
|
deleteSeed
|
|
290
316
|
};
|
|
291
|
-
//# sourceMappingURL=chunk-
|
|
317
|
+
//# sourceMappingURL=chunk-JDH2LDFS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/seeds/pool.ts","../src/seeds/types.ts"],"sourcesContent":["// Material pool engine (design doc §4). All four eviction rules are\n// deterministic code - no LLM involvement (§4.2). Storage: data/seeds.jsonl,\n// one seed per JSON line, DPAPI-encrypted at rest (D14).\n\nimport path from 'node:path';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { loadEncryptedText, saveEncryptedText } from '../vault/vault.js';\nimport { withFileLock } from '../core/file-lock.js';\nimport type { Policy } from '../config/schema.js';\nimport {\n SEED_SOURCE_DEFAULT_CONFIDENCE,\n emptySeedDb,\n type Seed,\n type SeedCategory,\n type SeedDb,\n type SeedRetireReason,\n type SeedSource,\n type SeedTag,\n} from './types.js';\n\nconst DAY_MS = 86_400_000;\n\n// ── lifecycle audit (2026-10-06 / v1.9.0) ───────────────────────────────\n// The pool used to mutate in silence: a seed could appear, be evicted for\n// capacity, or be dropped from the archive with nothing in the log. Every\n// mutator now takes an optional sink and reports what it did. IDs and\n// metadata ONLY — heartbeat.jsonl is plaintext by charter and must never\n// carry conversation text; the seed text itself stays in the DPAPI pool.\nexport type SeedAuditFn = (entry: Record<string, unknown>) => void;\n\nconst NO_AUDIT: SeedAuditFn = () => {};\n\n/** Audit must never break the pool (or the beat that called it). */\nfunction emit(audit: SeedAuditFn, entry: Record<string, unknown>): void {\n try {\n audit(entry);\n } catch {\n /* audit is diagnostic; a failed append is not a pool failure */\n }\n}\n\n// Per-category pool caps (2026-09-18 spec ⑤): topic stock vs conversation-grown\n// chat material, decoupled from `source`; sum == the global maxActive default.\nexport const SEED_CATEGORY_CAPS: Record<SeedCategory, number> = { topic: 16, chat: 14 };\n\nconst CATEGORY_VALUES: readonly string[] = ['topic', 'chat'];\n\n/** Lenient category coercion for on-disk rows: anything missing/unknown (all\n * pre-v1.5 seeds) counts as topic stock. */\nexport function normalizeCategory(v: unknown): SeedCategory {\n return typeof v === 'string' && (CATEGORY_VALUES as readonly string[]).includes(v)\n ? (v as SeedCategory)\n : 'topic';\n}\n\nexport interface AddSeedInput {\n text: string;\n topic?: string;\n tag?: SeedTag;\n source?: SeedSource;\n /** Loop role (spec ⑤); defaults from source: chat->chat, everything else->topic. */\n category?: SeedCategory;\n confidence?: number;\n}\n\nexport interface AddSeedResult {\n kind: 'added' | 'merged' | 'duplicate';\n seed: Seed;\n evicted?: Seed;\n}\n\nexport interface GcReport {\n consumed: number;\n expired: number;\n coldBench: number;\n activeAfter: number;\n /** v1.9.0: archived rows dropped to honor seeds.archiveCap (oldest first). */\n archiveTrimmed: number;\n}\n\nexport const TTL_KEYS: readonly SeedTag[] = ['news', 'fandom', 'scene', 'promise'];\n\nexport function normalizeTag(tag: string | undefined): SeedTag {\n if (tag && (TTL_KEYS as readonly string[]).includes(tag)) return tag as SeedTag;\n return 'scene';\n}\n\nfunction parseIso(v: string): number {\n const t = Date.parse(v);\n return Number.isFinite(t) ? t : 0;\n}\n\n// ── storage ─────────────────────────────────────────────────────────────\n\nexport function seedsFilePath(dataDir: string): string {\n return path.join(dataDir, 'seeds.jsonl');\n}\n\nexport function loadPool(guard: PathGuard, file: string): SeedDb {\n const raw = loadEncryptedText(guard, file);\n if (raw === null) return emptySeedDb();\n const db = emptySeedDb();\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const obj = JSON.parse(trimmed) as Seed;\n if (typeof obj.id === 'string' && obj.id.startsWith('s')) {\n // spec ⑤ loadPool tolerance: only the id is load-bearing; a missing or\n // unknown category (every pre-v1.5 row) falls back to topic stock.\n obj.category = normalizeCategory(obj.category);\n db.seeds.push(obj);\n const n = Number(obj.id.slice(1));\n if (Number.isFinite(n) && n > db.seq) db.seq = n;\n }\n } catch {\n // corrupt line: skip (pool is a cache; journal-level audit lives elsewhere)\n }\n }\n return db;\n}\n\nexport function savePool(guard: PathGuard, file: string, db: SeedDb): void {\n const body = db.seeds.map((s) => JSON.stringify(s)).join('\\n');\n saveEncryptedText(guard, file, body ? body + '\\n' : '');\n}\n\n/**\n * H-20 (minimal tier): every pool writer goes through here, so the load happens\n * INSIDE the cross-process lock and cannot be based on a read that went stale\n * while it was being mutated — the CLI is a second process sharing this file,\n * and an interleaved write used to be silently overwritten. The ciphertext is a\n * whole-file rewrite, so append/merge does not apply here; the append-only pool\n * with compaction is v2.0 work (ledger #8649f9).\n */\nfunction update<T>(guard: PathGuard, file: string, fn: (db: SeedDb) => T): T {\n return withFileLock(file, () => fn(loadPool(guard, file)));\n}\n\n// ── queries ─────────────────────────────────────────────────────────────\n\nexport const activeSeeds = (db: SeedDb): Seed[] => db.seeds.filter((s) => s.status === 'active');\nexport const archivedSeeds = (db: SeedDb): Seed[] => db.seeds.filter((s) => s.status === 'archived');\n\n// ── eviction scoring (rule 4) ───────────────────────────────────────────\n\n/**\n * Comprehensive score (§4.2): freshness x0.4 + unused x0.3 + confidence x0.3.\n * Higher = keep. Lowest-scoring active is evicted when the pool is full.\n */\nexport function evictionScore(seed: Seed, policy: Policy, now: number): number {\n const ttlDays = policy.seeds.ttlDays[seed.tag] || 14;\n const daysStale = Math.max(0, (now - parseIso(seed.lastEvidenceAt)) / DAY_MS);\n const freshness = Math.max(0, 1 - daysStale / ttlDays);\n const unused = seed.used === 0 ? 1 : 1 / seed.used;\n const w = policy.seeds.scoreWeights;\n return freshness * w.freshness + unused * w.unused + seed.confidence * w.confidence;\n}\n\n/** Pick the eviction victim: unprotected actives first (§4.2 加权保护); with\n * `category`, only that category's actives compete (spec ⑤ per-category caps). */\nexport function pickEvictionVictim(db: SeedDb, policy: Policy, now: number, category?: SeedCategory): Seed | null {\n const actives = activeSeeds(db).filter((s) => category === undefined || normalizeCategory(s.category) === category);\n if (actives.length === 0) return null;\n const unprotected = actives.filter((s) => !s.protected);\n const candidates = unprotected.length > 0 ? unprotected : actives;\n let worst: Seed | null = null;\n let worstScore = Number.POSITIVE_INFINITY;\n for (const s of candidates) {\n const score = evictionScore(s, policy, now);\n if (score < worstScore) {\n worstScore = score;\n worst = s;\n }\n }\n return worst;\n}\n\nfunction archiveSeed(seed: Seed, reason: SeedRetireReason, now: number): void {\n seed.status = 'archived';\n seed.retireReason = reason;\n seed.retiredAt = new Date(now).toISOString();\n}\n\n/** Spec ④: chat-grown material is consumed ONCE (conversation freshness);\n * every other category follows the policy-wide retireAfterUsed. */\nfunction retireLimit(s: Seed, policy: Policy): number {\n return normalizeCategory(s.category) === 'chat' ? 1 : policy.seeds.retireAfterUsed;\n}\n\n/**\n * Rule 1 (§4.2): used up to its limit AND no evidence newer than the PREVIOUS\n * use. `prevUsedAt` MUST be read before bumping `lastUsedAt` — comparing the\n * just-written value against `lastEvidenceAt` is vacuously true (H-22), which\n * retired seeds that had received fresh evidence in the same beat. Both\n * callers (gc and surface) share this, so the rule cannot drift again.\n *\n * `prevUsedAt === 0` means \"never used before\": there is no earlier use for\n * the evidence to be newer THAN, so the exemption cannot apply — a one-shot\n * (chat) seed still retires on its first surfacing (spec ④), which is the\n * contract, not a regression.\n */\nfunction isConsumed(s: Seed, policy: Policy, prevUsedAt: number, sinceEvidence: number): boolean {\n if (s.used < retireLimit(s, policy)) return false;\n return !(prevUsedAt > 0 && sinceEvidence > prevUsedAt);\n}\n\n// ── operations ──────────────────────────────────────────────────────────\n\nexport function addSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n input: AddSeedInput,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): AddSeedResult {\n // H-20: the read-modify-write below runs under the cross-process pool lock\n // (same guard as `update`, kept as a named body to avoid re-indenting it).\n return withFileLock(file, () => addSeedLocked(guard, file, policy, input, now, audit));\n}\n\nfunction addSeedLocked(\n guard: PathGuard,\n file: string,\n policy: Policy,\n input: AddSeedInput,\n now: number,\n audit: SeedAuditFn,\n): AddSeedResult {\n const db = loadPool(guard, file);\n const text = input.text.trim();\n if (!text) throw new Error('seed text must not be empty');\n const tag = normalizeTag(input.tag);\n const source: SeedSource = input.source ?? 'chat';\n const category = normalizeCategory(input.category ?? (source === 'chat' ? 'chat' : 'topic'));\n const confidence = input.confidence ?? SEED_SOURCE_DEFAULT_CONFIDENCE[source];\n\n // Exact-text dedup against actives.\n const dup = activeSeeds(db).find((s) => s.text === text);\n if (dup) return { kind: 'duplicate', seed: dup };\n\n const nowIso = new Date(now).toISOString();\n const ttlMs = (policy.seeds.ttlDays[tag] || 14) * DAY_MS;\n\n // Same-topic merge (§4.3): collapse all same-topic actives + the new item.\n const sameTopic = activeSeeds(db).filter((s) => s.topic === (input.topic ?? text.slice(0, 24)));\n let seed: Seed;\n if (sameTopic.length > 0) {\n const kept = sameTopic[0]!;\n kept.text = text; // brief takes the latest\n kept.tag = tag;\n kept.source = source;\n kept.category = normalizeCategory(input.category ?? kept.category ?? category);\n kept.confidence = Math.max(kept.confidence, confidence);\n kept.used = sameTopic.reduce((acc, s) => acc + s.used, 0); // used accumulates\n kept.lastEvidenceAt = [kept.lastEvidenceAt, nowIso, ...sameTopic.map((s) => s.lastEvidenceAt)]\n .reduce((a, b) => (parseIso(b) > parseIso(a) ? b : a));\n kept.expiresAt = new Date(Math.max(parseIso(kept.expiresAt), now + ttlMs)).toISOString();\n kept.protected = kept.protected || source === 'hand' || (source === 'profile' && confidence >= 0.7);\n // remove the other same-topic actives (merged into kept)\n for (const extra of sameTopic.slice(1)) {\n extra.status = 'archived';\n extra.retireReason = 'completed';\n extra.retiredAt = nowIso;\n emit(audit, { event: 'seed_retired', id: extra.id, reason: 'completed', via: 'merge', into: kept.id });\n }\n seed = kept;\n // capacity still applies after growth check below if kept is somehow over cap\n if (activeSeeds(db).length > policy.seeds.maxActive) {\n const victim = pickEvictionVictim(db, policy, now, normalizeCategory(kept.category));\n if (victim && victim.id !== kept.id) {\n archiveSeed(victim, 'pool_cap', now);\n emit(audit, { event: 'seed_added', id: kept.id, kind: 'merged', tag: kept.tag, source: kept.source, category: kept.category, confidence: kept.confidence, mergedFrom: sameTopic.length - 1 });\n emit(audit, { event: 'seed_retired', id: victim.id, reason: 'pool_cap', via: 'merge', into: kept.id });\n savePool(guard, file, db);\n return { kind: 'merged', seed: kept, evicted: victim };\n }\n }\n emit(audit, { event: 'seed_added', id: kept.id, kind: 'merged', tag: kept.tag, source: kept.source, category: kept.category, confidence: kept.confidence, mergedFrom: sameTopic.length - 1 });\n savePool(guard, file, db);\n return { kind: 'merged', seed: kept };\n }\n\n // Capacity first (rules 4 + spec ⑤): per-category cap evicts within its own\n // pool; the global cap evicts in the incoming seed's category (a full topic\n // stock must not push chat material out and vice versa).\n let evicted: Seed | undefined;\n const catCount = activeSeeds(db).filter((s) => normalizeCategory(s.category) === category).length;\n if (catCount >= SEED_CATEGORY_CAPS[category]) {\n const victim = pickEvictionVictim(db, policy, now, category);\n if (victim) {\n archiveSeed(victim, 'pool_cap', now);\n evicted = victim;\n }\n }\n if (activeSeeds(db).length >= policy.seeds.maxActive) {\n const victim = pickEvictionVictim(db, policy, now, category);\n if (victim) {\n archiveSeed(victim, 'pool_cap', now);\n evicted = evicted ?? victim;\n }\n }\n\n db.seq += 1;\n seed = {\n id: `s${db.seq}`,\n text,\n topic: input.topic ?? text.slice(0, 24),\n tag,\n source,\n category,\n confidence,\n protected: source === 'hand' || (source === 'profile' && confidence >= 0.7),\n used: 0,\n bornAt: nowIso,\n expiresAt: new Date(now + ttlMs).toISOString(),\n lastUsedAt: null,\n lastEvidenceAt: nowIso,\n status: 'active',\n };\n db.seeds.push(seed);\n emit(audit, { event: 'seed_added', id: seed.id, kind: 'added', tag: seed.tag, source: seed.source, category: seed.category, confidence: seed.confidence });\n if (evicted) emit(audit, { event: 'seed_retired', id: evicted.id, reason: 'pool_cap', via: 'add', into: seed.id });\n savePool(guard, file, db);\n return { kind: 'added', seed, evicted };\n}\n\n/**\n * Deterministic gc (rules 1-3 of §4.2). Rule 4 (pool cap) fires on add only.\n * v1.9.0: the archive area is ALSO capped (seeds.archiveCap) — the archive is\n * a forensic trail, not a hoard; trim oldest-retired first.\n */\nexport function gcPool(guard: PathGuard, file: string, policy: Policy, now = Date.now(), audit: SeedAuditFn = NO_AUDIT): GcReport {\n return update(guard, file, (db) => {\n const report: GcReport = { consumed: 0, expired: 0, coldBench: 0, activeAfter: 0, archiveTrimmed: 0 };\n for (const s of activeSeeds(db)) {\n const ageMs = now - parseIso(s.bornAt);\n const sinceEvidence = parseIso(s.lastEvidenceAt);\n const sinceUsed = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;\n if (isConsumed(s, policy, sinceUsed, sinceEvidence)) {\n archiveSeed(s, 'consumed', now);\n report.consumed += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'consumed', via: 'gc', used: s.used });\n } else if (now > parseIso(s.expiresAt)) {\n archiveSeed(s, 'expired', now);\n report.expired += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'expired', via: 'gc', used: s.used });\n } else if (s.used === 0 && ageMs >= policy.seeds.coldBenchDays * DAY_MS) {\n archiveSeed(s, 'cold_bench', now);\n report.coldBench += 1;\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'cold_bench', via: 'gc', used: s.used });\n }\n }\n // archive cap: drop the oldest-retired rows beyond the cap (retiredAt may be\n // missing on pre-v1.5 rows — parseIso('') = 0 sorts them oldest).\n const archived = db.seeds.filter((s) => s.status === 'archived');\n if (archived.length > policy.seeds.archiveCap) {\n const drop = [...archived]\n .sort((a, b) => parseIso(a.retiredAt ?? '') - parseIso(b.retiredAt ?? ''))\n .slice(0, archived.length - policy.seeds.archiveCap);\n const dropped = new Set(drop.map((s) => s.id));\n db.seeds = db.seeds.filter((s) => !dropped.has(s.id));\n report.archiveTrimmed = drop.length;\n // These rows are GONE (not archived — deleted): the log is the only trace\n // left, so name them. IDs only, and cap the list in case a first GC trims\n // a legacy archive wholesale.\n emit(audit, { event: 'seed_archive_trimmed', count: drop.length, ids: drop.slice(0, 50).map((s) => s.id) });\n }\n report.activeAfter = activeSeeds(db).length;\n savePool(guard, file, db);\n return report;\n });\n}\n\n/** The seed surfaced in a real expression: count + maybe retire (rule 1). */\nexport function surfaceSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n id: string,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): Seed | null {\n return update(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'active');\n if (!s) return null;\n // H-22: snapshot the PREVIOUS use time before bumping it — the retirement\n // check must compare against the last use, not against this one.\n const prevUsedAt = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;\n s.used += 1;\n s.lastUsedAt = new Date(now).toISOString();\n if (isConsumed(s, policy, prevUsedAt, parseIso(s.lastEvidenceAt))) {\n archiveSeed(s, 'consumed', now);\n emit(audit, { event: 'seed_retired', id: s.id, reason: 'consumed', via: 'surface', used: s.used });\n }\n savePool(guard, file, db);\n return s;\n });\n}\n\n/** Deliberate retirement (item completed / no longer relevant). */\nexport function archiveSeedById(\n guard: PathGuard,\n file: string,\n id: string,\n reason: SeedRetireReason = 'completed',\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): Seed | null {\n return update(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'active');\n if (!s) return null;\n archiveSeed(s, reason, now);\n emit(audit, { event: 'seed_retired', id: s.id, reason, via: 'manual' });\n savePool(guard, file, db);\n return s;\n });\n}\n\n/** Restore an archived seed to the active pool (UI operation; resets TTL). */\nexport function restoreSeed(\n guard: PathGuard,\n file: string,\n policy: Policy,\n id: string,\n now = Date.now(),\n audit: SeedAuditFn = NO_AUDIT,\n): { ok: true; seed: Seed } | { ok: false; reason: string } {\n return update<{ ok: true; seed: Seed } | { ok: false; reason: string }>(guard, file, (db) => {\n const s = db.seeds.find((x) => x.id === id && x.status === 'archived');\n if (!s) return { ok: false, reason: 'archived seed not found' };\n // H-23: a restore must satisfy the same two caps an add does — the global\n // one AND the per-category one. The add path evicts a neighbour instead, but\n // restoring a memory is a user action: refuse it rather than silently\n // pushing someone else's material out.\n const category = normalizeCategory(s.category);\n const catCap = SEED_CATEGORY_CAPS[category];\n const inCategory = activeSeeds(db).filter((x) => normalizeCategory(x.category) === category).length;\n if (inCategory >= catCap) {\n return { ok: false, reason: `category full (${category}: ${catCap}); archive something in it first` };\n }\n if (activeSeeds(db).length >= policy.seeds.maxActive) {\n return { ok: false, reason: `pool full (${policy.seeds.maxActive}); archive something first` };\n }\n s.status = 'active';\n s.retireReason = undefined;\n s.retiredAt = undefined;\n // H-23: reset bornAt as well — cold-bench eviction (used === 0 && age >=\n // coldBenchDays) keys off bornAt, so a restored seed used to be re-archived\n // by the very next maintenance beat (\"restored, then gone one beat later\").\n s.bornAt = new Date(now).toISOString();\n s.expiresAt = new Date(now + (policy.seeds.ttlDays[s.tag] || 14) * DAY_MS).toISOString();\n s.lastEvidenceAt = new Date(now).toISOString();\n emit(audit, { event: 'seed_restored', id: s.id });\n savePool(guard, file, db);\n return { ok: true, seed: s };\n });\n}\n\n/** Hard delete (UI explicit action with confirm; the caller audits it). */\nexport function deleteSeed(guard: PathGuard, file: string, id: string, audit: SeedAuditFn = NO_AUDIT): boolean {\n return update(guard, file, (db) => {\n const before = db.seeds.length;\n db.seeds = db.seeds.filter((x) => x.id !== id);\n if (db.seeds.length === before) return false;\n emit(audit, { event: 'seed_deleted', id });\n savePool(guard, file, db);\n return true;\n });\n}\n","// Material pool (\"素材池\") types. Charter (axiom 3): this pool is a CACHE,\n// not memory - volatile, evictable, burnable; long-term memory only absorbs\n// conversation-validated content. Everything here may be destroyed.\n\nexport type SeedTag = 'news' | 'fandom' | 'scene' | 'promise';\nexport type SeedSource = 'chat' | 'screen' | 'browse' | 'hand' | 'profile';\nexport type SeedCategory = 'topic' | 'chat';\nexport type SeedRetireReason = 'consumed' | 'expired' | 'cold_bench' | 'pool_cap' | 'completed';\n\nexport interface Seed {\n id: string;\n /** Short brief shown to the reflection prompt. */\n text: string;\n /** Merge key (§4.3): same-topic actives collapse into one. */\n topic: string;\n tag: SeedTag;\n source: SeedSource;\n /** Loop role, decoupled from source (2026-09-18 spec ⑤): 'topic' = wander/\n * hand/profile stock (random pick), 'chat' = conversation-grown, consumed\n * once. Absent on pre-v1.5 rows; loadPool defaults them to 'topic'. */\n category?: SeedCategory;\n /** 0..1 trust level; handwriting and high-confidence profile sources gain eviction protection. */\n confidence: number;\n /** Eviction protection (§4.2 pool-cap rule): handwritten / high-confidence profile sources. */\n protected: boolean;\n /** Times this seed surfaced in an actual expression. */\n used: number;\n bornAt: string;\n /** bornAt + ttlDays[tag]; archived when passed (rule 2). */\n expiresAt: string;\n lastUsedAt: string | null;\n /** Fresh-evidence watermark: rule 1 spares seeds with evidence newer than last use. */\n lastEvidenceAt: string;\n status: 'active' | 'archived';\n retireReason?: SeedRetireReason;\n retiredAt?: string;\n}\n\nexport interface SeedDb {\n seq: number;\n seeds: Seed[];\n}\n\nexport function emptySeedDb(): SeedDb {\n return { seq: 0, seeds: [] };\n}\n\nexport const SEED_SOURCE_DEFAULT_CONFIDENCE: Record<SeedSource, number> = {\n hand: 1.0,\n profile: 0.7,\n chat: 0.6,\n screen: 0.4,\n browse: 0.4,\n};\n"],"mappings":"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","names":[]}
|