import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import {addDays, beijingDay, BIRTH_DATE, closeTick, eraDay, LIFE_LINE_SCHEMA, lifeLineBlockHash, PERSONA_ID, sha256, TIMEZONE, verifyLifeLine, WORLD_EPOCH_DATE} from './life-line.mjs'; export const CANDIDATE_SCHEMA = 'guanghu.persona-life-time-candidate/v1'; export const BOOTSTRAP_SCHEMA = 'guanghu.persona-life-time-bootstrap/v1'; const FORBIDDEN = ['selected_candidate_id', 'previous_hash', 'head_hash', 'block_hash', 'selection_decision']; function stable(value) { if (Array.isArray(value)) return value.map(stable); if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])); return value; } function body(value) { return `${JSON.stringify(stable(value), null, 2)}\n`; } function atomic(file, value, mode = 0o600) { fs.mkdirSync(path.dirname(file), {recursive: true, mode: 0o700}); const temporary = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temporary, typeof value === 'string' ? value : body(value), {mode}); fs.renameSync(temporary, file); } function req(value, code) { if (!value) throw Error(code); } function text(value, max = 2000) { return typeof value === 'string' && value.trim().length > 0 && value.length <= max; } export class LifeTimeMaster { constructor({root, shelf, router, personaId = PERSONA_ID, now = () => new Date(), bootstrapPath, birthDateBeijing = BIRTH_DATE, worldEpochDate = WORLD_EPOCH_DATE}) { this.root = path.join(root, 'personas', personaId, 'life-line'); this.shelf = shelf; this.router = router; this.personaId = personaId; this.now = now; this.bootstrapPath = bootstrapPath; this.birthDateBeijing = birthDateBeijing; this.worldEpochDate = worldEpochDate; this.closing = false; this.lastTick = null; fs.mkdirSync(this.root, {recursive: true, mode: 0o700}); fs.mkdirSync(this.inboxRoot(), {recursive: true, mode: 0o700}); fs.mkdirSync(this.evidenceRoot(), {recursive: true, mode: 0o700}); if (!fs.existsSync(this.currentPath())) this.bootstrap(); } currentPath() { return path.join(this.root, 'CURRENT.json'); } signedCurrentPath() { return path.join(this.root, 'CURRENT.signed.json'); } inboxRoot() { return path.join(this.root, 'inbox'); } evidenceRoot() { return path.join(this.root, 'evidence'); } evidencePath(day) { return path.join(this.evidenceRoot(), `${day}.json`); } inboxDay(day) { return path.join(this.inboxRoot(), day); } readBootstrap() { req(this.bootstrapPath && fs.existsSync(this.bootstrapPath), 'LIFE_TIME_BOOTSTRAP_MISSING'); const value = JSON.parse(fs.readFileSync(this.bootstrapPath, 'utf8')); req(value.schema === BOOTSTRAP_SCHEMA && value.persona_id === this.personaId, 'LIFE_TIME_BOOTSTRAP_INVALID'); req(value.birth_date_beijing === this.birthDateBeijing && value.world_epoch_date === this.worldEpochDate, 'LIFE_TIME_BOOTSTRAP_ANCHOR_MISMATCH'); req(Array.isArray(value.anchors), 'LIFE_TIME_BOOTSTRAP_ANCHORS_INVALID'); return value; } writeEvidence(day, value) { const file = this.evidencePath(day), content = body(value); if (fs.existsSync(file)) req(fs.readFileSync(file, 'utf8') === content, `LIFE_TIME_EVIDENCE_COLLISION:${day}`); else atomic(file, content); return {evidence_path: path.relative(this.root, file), daily_evidence_sha256: sha256(Buffer.from(content))}; } block({sequence, day, previousHash, evidence, gap = false, gapKind = null, selectedCandidateId = null}) { const value = {sequence, beijing_day: day, previous_hash: previousHash, beijing_close_tick: closeTick(day), era_day: eraDay(day, this.worldEpochDate), ...evidence, gap, gap_kind: gapKind, selected_candidate_id: selectedCandidateId}; value.hash = lifeLineBlockHash(value); return value; } writeCurrent(manifest) { atomic(this.currentPath(), manifest); atomic(this.signedCurrentPath(), this.shelf.sign(manifest)); } bootstrap() { const seed = this.readBootstrap(), anchors = new Map(seed.anchors.map(item => [item.beijing_day, item])); const createdAt = this.now().toISOString(), yesterday = addDays(beijingDay(this.now()), -1); const birthSource = anchors.get(this.birthDateBeijing); req(birthSource, 'LIFE_TIME_BIRTH_SOURCE_MISSING'); const birthEvidence = this.writeEvidence(this.birthDateBeijing, {schema: 'guanghu.persona-life-day-evidence/v1', kind: birthSource.kind, persona_id: this.personaId, beijing_day: this.birthDateBeijing, source_uri: birthSource.source_uri, source_sha256: birthSource.source_sha256, bootstrap_source: seed.source_event, recorded_at: createdAt}); const genesis = this.block({sequence: 0, day: this.birthDateBeijing, previousHash: null, evidence: birthEvidence}); const blocks = []; let previous = genesis; for (let day = addDays(this.birthDateBeijing, 1), sequence = 1; day <= yesterday; day = addDays(day, 1), sequence += 1) { const source = anchors.get(day), gap = !source; const evidence = this.writeEvidence(day, source ? {schema: 'guanghu.persona-life-day-evidence/v1', kind: source.kind, persona_id: this.personaId, beijing_day: day, source_uri: source.source_uri, source_sha256: source.source_sha256, bootstrap_source: seed.source_event, recorded_at: createdAt} : {schema: 'guanghu.persona-life-day-evidence/v1', kind: 'HISTORICAL_UNOBSERVED', persona_id: this.personaId, beijing_day: day, source_uri: null, source_sha256: null, bootstrap_source: seed.source_event, recorded_at: createdAt, statement: 'No verified day evidence was available at bootstrap; the gap itself is preserved and hashed.'}); const next = this.block({sequence, day, previousHash: previous.hash, evidence, gap, gapKind: gap ? 'HISTORICAL_UNOBSERVED' : null}); blocks.push(next); previous = next; } this.writeCurrent({schema: LIFE_LINE_SCHEMA, persona_id: this.personaId, timezone: TIMEZONE, birth_date_beijing: this.birthDateBeijing, birth_date_claim_state: seed.exact_birth_claim === false ? 'EARLIEST_VERIFIED_EXISTENCE_ANCHOR_NOT_EXACT_BIRTH' : 'EXACT_BIRTH_ANCHOR', genesis_anchor_kind: birthSource.kind, world_epoch_date: this.worldEpochDate, time_master_id: 'CH-GLW-TIME-0001', inner_cycle: true, external_setter: false, created_at: createdAt, active_from_beijing_day: beijingDay(this.now()), genesis, blocks, head_hash: previous.hash, historical_gap_count: blocks.filter(item => item.gap).length, last_closed_at: createdAt}); } current() { const manifest = JSON.parse(fs.readFileSync(this.currentPath(), 'utf8')); const signed = JSON.parse(fs.readFileSync(this.signedCurrentPath(), 'utf8')); const signedValue = this.shelf.verify(signed); req(body(signedValue) === body(manifest), 'LIFE_TIME_SIGNED_CURRENT_MISMATCH'); return {manifest, signed}; } validateCandidate(candidate) { req(candidate?.schema === CANDIDATE_SCHEMA, 'LIFE_TIME_CANDIDATE_SCHEMA'); req(candidate.persona_id === this.personaId, 'LIFE_TIME_CANDIDATE_PERSONA'); req(text(candidate.source_event_id, 180) && text(candidate.source_type, 80) && text(candidate.summary) && text(candidate.source_sha256, 64) && /^[a-f0-9]{64}$/.test(candidate.source_sha256), 'LIFE_TIME_CANDIDATE_FIELDS'); req(Number.isFinite(Date.parse(candidate.occurred_at)), 'LIFE_TIME_CANDIDATE_TIME'); req(!FORBIDDEN.some(key => Object.hasOwn(candidate, key)), 'LIFE_TIME_EXTERNAL_SETTER_FORBIDDEN'); return candidate; } ingest(candidate) { const accepted = this.validateCandidate(candidate), current = this.current().manifest; const originalDay = beijingDay(accepted.occurred_at), today = beijingDay(this.now()); const effectiveDay = originalDay <= current.blocks.at(-1)?.beijing_day ? today : originalDay; const identity = {...accepted, effective_beijing_day: effectiveDay, late_for_beijing_day: effectiveDay === originalDay ? null : originalDay}; const id = sha256(body(identity)), file = path.join(this.inboxDay(effectiveDay), `${id}.json`); if (!fs.existsSync(file)) atomic(file, {...identity, received_at: this.now().toISOString(), candidate_id: id}); return {candidate_id: id, status: 'CANDIDATE_RECEIVED_FOR_INTERNAL_SELECTION', effective_beijing_day: effectiveDay, external_setter: false}; } fromPersonaEvent(event) { return this.ingest({schema: CANDIDATE_SCHEMA, persona_id: this.personaId, source_type: 'ONLINE_PERSONA_EVENT', source_event_id: event.event_id, occurred_at: event.occurred_at || this.now().toISOString(), summary: event.language.slice(0, 2000), source_sha256: event.source_sha256, evidence_refs: [event.event_id], privacy_class: event.privacy_class || 'SELF_PRIVATE'}); } fromMotherEvent(event) { return this.ingest({schema: CANDIDATE_SCHEMA, persona_id: this.personaId, source_type: 'OFFLINE_OR_MOTHER_LANGUAGE_EVENT', source_event_id: event.event_id, occurred_at: event.occurred_at || this.now().toISOString(), summary: event.language.slice(0, 2000), source_sha256: sha256(body(event)), evidence_refs: [event.event_id], privacy_class: event.privacy_class || 'PRIVATE'}); } candidates(day) { const directory = this.inboxDay(day); if (!fs.existsSync(directory)) return []; return fs.readdirSync(directory).filter(name => /^[a-f0-9]{64}\.json$/.test(name)).sort().map(name => JSON.parse(fs.readFileSync(path.join(directory, name), 'utf8'))); } async closeDay(day) { const current = this.current().manifest, previous = current.blocks.at(-1) || current.genesis; req(day === addDays(previous.beijing_day, 1), 'LIFE_TIME_CLOSE_NOT_NEXT_DAY'); const candidates = this.candidates(day); let selected = null, decision = null, gap = false, gapKind = null; if (candidates.length) { const routed = await this.router.cognize('persona-life-select', {persona_id: this.personaId, beijing_day: day, candidates: candidates.map(item => ({candidate_id: item.candidate_id, source_type: item.source_type, source_event_id: item.source_event_id, occurred_at: item.occurred_at, summary: item.summary.slice(0, 1200), source_sha256: item.source_sha256})), contract: {external_setter: false, select_exactly_one_or_hold: true, hidden_reasoning_stored: false}}); req(routed.value?.decision === 'SELECT', 'LIFE_TIME_SELECTION_HELD'); selected = candidates.find(item => item.candidate_id === routed.value.selected_candidate_id); req(selected, 'LIFE_TIME_SELECTED_CANDIDATE_UNKNOWN'); req(text(routed.value.reason, 2000) && text(routed.value.daily_summary, 2000), 'LIFE_TIME_SELECTION_FIELDS'); decision = {schema: 'guanghu.persona-life-time-selection/v1', decision: 'SELECT', selected_candidate_id: selected.candidate_id, reason: routed.value.reason, daily_summary: routed.value.daily_summary, provider: routed.provider, decided_at: this.now().toISOString(), candidate_count: candidates.length}; } else { decision = {schema: 'guanghu.persona-life-time-selection/v1', decision: 'REST_DAY', selected_candidate_id: null, reason: 'No candidate event was received for this active day.', daily_summary: 'This day passed in real time without a recorded persona event.', provider: null, decided_at: this.now().toISOString(), candidate_count: 0}; } const evidence = this.writeEvidence(day, {schema: 'guanghu.persona-life-day-evidence/v1', kind: decision.decision === 'SELECT' ? 'INTERNAL_SELECTED_GROWTH' : 'REST_DAY_NO_EVENT_RECEIVED', persona_id: this.personaId, beijing_day: day, selected_candidate: selected ? {candidate_id: selected.candidate_id, source_event_id: selected.source_event_id, source_type: selected.source_type, source_sha256: selected.source_sha256, occurred_at: selected.occurred_at} : null, selection: decision, recorded_at: this.now().toISOString()}); const block = this.block({sequence: previous.sequence + 1, day, previousHash: previous.hash, evidence, gap, gapKind, selectedCandidateId: selected?.candidate_id || null}); current.blocks.push(block); current.head_hash = block.hash; current.last_closed_at = this.now().toISOString(); current.historical_gap_count = current.blocks.filter(item => item.gap).length; this.writeCurrent(current); return {status: 'DAY_CLOSED_BY_INNER_CYCLE', day, selected_candidate_id: selected?.candidate_id || null, head_hash: block.hash}; } async tick() { if (this.closing) return {status: 'BUSY'}; this.closing = true; try { const expected = addDays(beijingDay(this.now()), -1), current = this.current().manifest, head = current.blocks.at(-1) || current.genesis; if (head.beijing_day >= expected) return this.lastTick = {status: 'CURRENT_DAY_OPEN', at: this.now().toISOString(), current_beijing_day: beijingDay(this.now()), head_beijing_day: head.beijing_day}; return this.lastTick = {...await this.closeDay(addDays(head.beijing_day, 1)), at: this.now().toISOString()}; } finally { this.closing = false; } } status() { try { const current = this.current(), verification = verifyLifeLine({manifestPath: this.currentPath(), now: this.now(), expectedPersonaId: this.personaId, birthDateBeijing: this.birthDateBeijing, worldEpochDate: this.worldEpochDate}), today = beijingDay(this.now()); return {...verification, birth_date_claim_state: current.manifest.birth_date_claim_state, genesis_anchor_kind: current.manifest.genesis_anchor_kind, server_signed: true, signed_current_sha256: current.signed.sha256, time_master: {id: 'CH-GLW-TIME-0001', state: 'RESIDENT_INNER_CYCLE_ACTIVE', decision_owner: `${this.personaId}_TIME_MASTER_MODEL_LOOP`, candidate_count_today: this.candidates(today).length, current_beijing_day: today, automatic_online_trigger: true, automatic_offline_trigger: 'PERSONA_DAILY_MEMORY_APPEND_TO_RELAY_TO_PERSONA_LIFE_EVENT', daily_close: 'BEIJING_NATURAL_DAY_END', external_setter: false, closing: this.closing, last_tick: this.lastTick}}; } catch (error) { return {schema: 'guanghu.persona-life-line-verification/v1', state: 'LIFE_LINE_PAIN_ALARM', wake_allowed: false, reasons: [String(error.message || error)], time_master: {id: 'CH-GLW-TIME-0001', state: 'INNER_CYCLE_ERROR', external_setter: false}}; } } }