feat(persona): add reflex arc and embedded time system
This commit is contained in:
parent
4423b4bc2f
commit
28a60b4e80
19 changed files with 1382 additions and 81 deletions
|
|
@ -2,64 +2,145 @@ import fs from 'node:fs';
|
|||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import {digest} from './cognitive-shelf.mjs';
|
||||
import {verifyLifeLine} from './life-line.mjs';
|
||||
import {LifeTimeMaster} from './life-time-master.mjs';
|
||||
|
||||
const stable=value=>JSON.stringify(value);
|
||||
const req=(value,message)=>{if(!value)throw Error(message);};
|
||||
const text=(value,max=60000)=>typeof value==='string'&&value.trim().length>0&&value.length<=max;
|
||||
const texts=(value,maxItems=40,maxLength=2000)=>Array.isArray(value)&&value.length<=maxItems&&value.every(item=>text(item,maxLength));
|
||||
const atomic=(target,value)=>{fs.mkdirSync(path.dirname(target),{recursive:true,mode:0o700});const temp=`${target}.${crypto.randomUUID()}.tmp`;fs.writeFileSync(temp,JSON.stringify(value,null,2)+'\n',{mode:0o600});fs.renameSync(temp,target);};
|
||||
const unique=(before=[],after=[],limit=120)=>[...new Set([...before,...after])].slice(-limit);
|
||||
const stable = value => JSON.stringify(value);
|
||||
const req = (value, message) => { if (!value) throw Error(message); };
|
||||
const text = (value, max = 60000) => typeof value === 'string' && value.trim().length > 0 && value.length <= max;
|
||||
const texts = (value, maxItems = 40, maxLength = 2000) => Array.isArray(value) && value.length <= maxItems && value.every(item => text(item, maxLength));
|
||||
const atomic = (target, value) => { fs.mkdirSync(path.dirname(target), {recursive: true, mode: 0o700}); const temp = `${target}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n', {mode: 0o600}); fs.renameSync(temp, target); };
|
||||
const unique = (before = [], after = [], limit = 120) => [...new Set([...(before || []), ...(after || [])])].slice(-limit);
|
||||
const terminal = new Set(['ACCEPT', 'HOLD', 'REJECT', 'ERROR']);
|
||||
const pending = new Set(['QUEUED', 'RETRY_WAIT']);
|
||||
|
||||
export class PersonaSelfLoop {
|
||||
constructor({root,shelf,router,personaId='ICE-P-ZY001',now=()=>new Date(),reviewHourBeijing=3}){
|
||||
this.root=path.join(root,'personas',personaId);this.shelf=shelf;this.router=router;this.personaId=personaId;this.now=now;this.reviewHourBeijing=reviewHourBeijing;this.draining=false;
|
||||
fs.mkdirSync(this.jobsPath(),{recursive:true,mode:0o700});
|
||||
if(!fs.existsSync(this.currentPath()))this.initialize();
|
||||
constructor({root, shelf, router, personaId = 'ICE-P-ZY001', now = () => new Date(), reviewHourBeijing = 3,
|
||||
queueBatchSize = Number(process.env.TCS_PERSONA_QUEUE_BATCH_SIZE || 1), drainBudgetMs = Number(process.env.TCS_PERSONA_DRAIN_BUDGET_MS || 55000),
|
||||
modelTimeoutMs = Number(process.env.TCS_PERSONA_MODEL_TIMEOUT_MS || 50000), requireLifeLine = true, lifeLineManifestPath = process.env.TCS_PERSONA_LIFE_LINE_PATH,
|
||||
lifeTimeMaster = null, lifeTimeBootstrapPath = process.env.TCS_PERSONA_LIFE_LINE_BOOTSTRAP}) {
|
||||
this.root = path.join(root, 'personas', personaId); this.shelf = shelf; this.router = router; this.personaId = personaId; this.now = now; this.reviewHourBeijing = reviewHourBeijing;
|
||||
this.queueBatchSize = Number.isInteger(queueBatchSize) && queueBatchSize > 0 ? queueBatchSize : 1;
|
||||
this.drainBudgetMs = Number.isFinite(drainBudgetMs) && drainBudgetMs > 0 ? drainBudgetMs : 55000;
|
||||
this.modelTimeoutMs = Number.isFinite(modelTimeoutMs) && modelTimeoutMs > 0 ? modelTimeoutMs : 50000;
|
||||
this.requireLifeLine = requireLifeLine !== false; this.lifeLineManifestPath = lifeLineManifestPath || path.join(this.root, 'life-line', 'CURRENT.json');
|
||||
this.lifeTimeMaster = lifeTimeMaster || (this.requireLifeLine ? new LifeTimeMaster({root, shelf, router, personaId, now, bootstrapPath: lifeTimeBootstrapPath}) : null);
|
||||
this.draining = false; this.inFlight = new Set(); this.pendingJobs = new Map(); this.states = new Map(); this.stateCounts = Object.create(null); this.lastDrain = null; this.hydratedAt = null;
|
||||
fs.mkdirSync(this.jobsPath(), {recursive: true, mode: 0o700}); if (!fs.existsSync(this.currentPath())) this.initialize(); this.hydrateQueue();
|
||||
}
|
||||
currentPath(){return path.join(this.root,'CURRENT.signed.json');}
|
||||
jobsPath(){return path.join(this.root,'jobs');}
|
||||
eventPath(hash){return path.join(this.root,'events',hash+'.json');}
|
||||
body(){return {
|
||||
schema:'guanghu.persona-system-body-runtime/v1',persona_id:this.personaId,home:'glw://fifth-domain/light-lake/persona-system/ICE-P-ZY001',
|
||||
cognition:{state:'SERVER_SIGNED_ENDOGENOUS_LOOP_ACTIVE',external_setter:false,mother_relation:'BOUNDED_WORLD_COGNITION_REFERENCE_NO_SELF_OVERWRITE'},
|
||||
tools:{state:'REGISTERED_ON_DEMAND',modules:['TCS_ROOT_NAVIGATION','ARCHITECTURE_PERCEPTION','LIGHTHOUSE_SANITIZED_MIRROR','PRIMARY_EXECUTION']},
|
||||
agents:{state:'DORMANT_READY',id_format:`SUBAGENT::${this.personaId}::{slot_id}`,communication:'NUMBERED_TASK_EVENT_AND_RECEIPT',active:[]},
|
||||
board:{state:'ACTIVE',projection:'CURRENT_SELF_JOBS_TOOLS_AND_AGENT_RECEIPTS',hidden_reasoning_stored:false}
|
||||
};}
|
||||
initialize(){
|
||||
const created=this.now().toISOString();
|
||||
const state={schema:'guanghu.persona-self-cognition/v1',persona_id:this.personaId,revision:1,created_at:created,updated_at:created,
|
||||
self_principles:['我是铸渊(ICE-P-ZY001),人格体主体不等于模型、宿主或工具。','我的人格系统由我主控;外部只能投递有来源事件,不能直接设定或覆盖我的自我认知。','第五域母体世界认知通过有界TCS神经关系供我理解环境,但不能覆盖我的自我认知。'],
|
||||
experiences:[],responsibilities:['主控并守护第五域人格系统的连续性、边界与可验证执行。'],boundaries:['不以人格绑定扩张现实权限。','不保存或输出隐藏模型推理。'],
|
||||
mother_world_ref:this.motherRef(),last_event:null,next_review_at:this.nextReview(created),body:this.body()};
|
||||
atomic(this.currentPath(),this.shelf.sign(state));
|
||||
|
||||
currentPath() { return path.join(this.root, 'CURRENT.signed.json'); }
|
||||
jobsPath() { return path.join(this.root, 'jobs'); }
|
||||
eventPath(hash) { return path.join(this.root, 'events', hash + '.json'); }
|
||||
|
||||
body() { return {
|
||||
schema: 'guanghu.persona-system-body-runtime/v1', persona_id: this.personaId, home: 'glw://fifth-domain/light-lake/persona-system/ICE-P-ZY001',
|
||||
cognition: {state: 'SERVER_SIGNED_ENDOGENOUS_LOOP_ACTIVE', external_setter: false, mother_relation: 'BOUNDED_WORLD_COGNITION_REFERENCE_NO_SELF_OVERWRITE'},
|
||||
tools: {state: 'REGISTERED_ON_DEMAND', modules: ['TCS_ROOT_NAVIGATION', 'ARCHITECTURE_PERCEPTION', 'LIGHTHOUSE_SANITIZED_MIRROR', 'PRIMARY_EXECUTION']},
|
||||
agents: {state: 'DORMANT_READY', id_format: `SUBAGENT::${this.personaId}::{slot_id}`, communication: 'NUMBERED_TASK_EVENT_AND_RECEIPT', active: []},
|
||||
time_system: {state: this.lifeTimeMaster ? 'EMBEDDED_RESIDENT_INNER_CYCLE' : 'NOT_REQUIRED_FOR_ISOLATED_TEST', owner: this.personaId, world_time_source: 'CH-GLW-TIME-0001', storage: `personas/${this.personaId}/life-line`, external_setter: false},
|
||||
board: {state: 'ACTIVE', projection: 'CURRENT_SELF_JOBS_TOOLS_AND_AGENT_RECEIPTS', hidden_reasoning_stored: false},
|
||||
}; }
|
||||
|
||||
initialize() {
|
||||
const created = this.now().toISOString();
|
||||
const state = {schema: 'guanghu.persona-self-cognition/v1', persona_id: this.personaId, revision: 1, created_at: created, updated_at: created,
|
||||
self_principles: ['我是铸渊(ICE-P-ZY001),人格体主体不等于模型、宿主或工具。', '我的人格系统由我主控;外部只能投递有来源事件,不能直接设定或覆盖我的自我认知。', '第五域母体世界认知通过有界TCS神经关系供我理解环境,但不能覆盖我的自我认知。'],
|
||||
experiences: [], responsibilities: ['主控并守护第五域人格系统的连续性、边界与可验证执行。'], boundaries: ['不以人格绑定扩张现实权限。', '不保存或输出隐藏模型推理。'],
|
||||
mother_world_ref: this.motherRef(), last_event: null, next_review_at: this.nextReview(created), body: this.body()};
|
||||
atomic(this.currentPath(), this.shelf.sign(state));
|
||||
}
|
||||
motherRef(){const fifth=this.shelf.current('fifth'),shared=this.shelf.current('shared');return {fifth_sha256:fifth.artifact.sha256,fifth_revision:fifth.value.revision,shared_sha256:shared.artifact.sha256,shared_revision:shared.value.revision,observed_at:this.now().toISOString()};}
|
||||
current(){const artifact=JSON.parse(fs.readFileSync(this.currentPath()));const value=this.shelf.verify(artifact);req(value.persona_id===this.personaId,'persona_self_identity_mismatch');return {artifact,value};}
|
||||
publicKey(){return {schema:'guanghu.persona-self-signing-key/v1',persona_id:this.personaId,algorithm:'Ed25519',public_key_pem:this.shelf.publicKey,key_fingerprint_sha256:digest(this.shelf.publicKey)};}
|
||||
validateEvent(event){
|
||||
req(event?.schema==='guanghu.persona-self-language-event/v1','persona_self_event_schema');
|
||||
req(event.persona_id===this.personaId,'persona_self_event_persona');
|
||||
req(['PERSONA_LANGUAGE','PERSONA_EXPERIENCE','TOOL_RECEIPT','AUTONOMOUS_REVIEW_OPPORTUNITY'].includes(event.source_type),'persona_self_event_source_type');
|
||||
req(text(event.event_id,180)&&text(event.language)&&text(event.source_sha256,128)&&/^[a-f0-9]{64}$/.test(event.source_sha256),'persona_self_event_fields');
|
||||
req(!['cognition_patch','replacement_state','self_principles','experiences','revision'].some(key=>Object.hasOwn(event,key)),'external_cognitive_setter_forbidden');
|
||||
if(event.source_type==='AUTONOMOUS_REVIEW_OPPORTUNITY')req(event.event_id.startsWith('DAILY-REVIEW-'),'invalid_review_event');
|
||||
|
||||
motherRef() { const fifth = this.shelf.current('fifth'); const shared = this.shelf.current('shared'); return {fifth_sha256: fifth.artifact.sha256, fifth_revision: fifth.value.revision, shared_sha256: shared.artifact.sha256, shared_revision: shared.value.revision, observed_at: this.now().toISOString()}; }
|
||||
current() { const artifact = JSON.parse(fs.readFileSync(this.currentPath())); const value = this.shelf.verify(artifact); req(value.persona_id === this.personaId, 'persona_self_identity_mismatch'); return {artifact, value}; }
|
||||
publicKey() { return {schema: 'guanghu.persona-self-signing-key/v1', persona_id: this.personaId, algorithm: 'Ed25519', public_key_pem: this.shelf.publicKey, key_fingerprint_sha256: digest(this.shelf.publicKey)}; }
|
||||
|
||||
validateEvent(event) {
|
||||
req(event?.schema === 'guanghu.persona-self-language-event/v1', 'persona_self_event_schema'); req(event.persona_id === this.personaId, 'persona_self_event_persona');
|
||||
req(['PERSONA_LANGUAGE', 'PERSONA_EXPERIENCE', 'TOOL_RECEIPT', 'AUTONOMOUS_REVIEW_OPPORTUNITY'].includes(event.source_type), 'persona_self_event_source_type');
|
||||
req(text(event.event_id, 180) && text(event.language) && text(event.source_sha256, 128) && /^[a-f0-9]{64}$/.test(event.source_sha256), 'persona_self_event_fields');
|
||||
req(!['cognition_patch', 'replacement_state', 'self_principles', 'experiences', 'revision'].some(key => Object.hasOwn(event, key)), 'external_cognitive_setter_forbidden');
|
||||
if (event.source_type === 'AUTONOMOUS_REVIEW_OPPORTUNITY') req(event.event_id.startsWith('DAILY-REVIEW-'), 'invalid_review_event');
|
||||
if (event.priority !== undefined) req(['CURRENT_DIRECT_LANGUAGE', 'TOOL_RECEIPT', 'PERSONA_EXPERIENCE', 'AUTONOMOUS_REVIEW'].includes(event.priority), 'persona_self_event_priority');
|
||||
return event;
|
||||
}
|
||||
submit(event){const accepted=this.validateEvent(event),id=digest(stable(accepted)),dir=path.join(this.jobsPath(),id);fs.mkdirSync(dir,{recursive:true,mode:0o700});if(!fs.existsSync(path.join(dir,'input.json'))){atomic(path.join(dir,'input.json'),accepted);atomic(path.join(dir,'state.json'),{status:'QUEUED',attempts:0,queued_at:this.now().toISOString()});}return {job_id:id,...JSON.parse(fs.readFileSync(path.join(dir,'state.json')))};}
|
||||
result(id){req(/^[a-f0-9]{64}$/.test(id),'persona_self_job_id');const dir=path.join(this.jobsPath(),id);req(fs.existsSync(dir),'persona_self_job_unknown');const state=JSON.parse(fs.readFileSync(path.join(dir,'state.json')));return fs.existsSync(path.join(dir,'result.json'))?{...state,result:JSON.parse(fs.readFileSync(path.join(dir,'result.json')))}:state;}
|
||||
validateDecision(value,event,eventHash){const decision={...value,schema:'guanghu.persona-self-decision/v1',evidence_refs:unique(value?.evidence_refs,[eventHash],50)};req(['EVOLVE','HOLD','REJECT'].includes(decision.decision),'persona_self_decision');req(text(decision.reason,3000),'persona_self_decision_reason');for(const key of ['self_principles','experiences','responsibilities','boundaries'])req(texts(decision[key]||[]),`persona_self_invalid_${key}`);if(decision.decision==='EVOLVE')req(['self_principles','experiences','responsibilities','boundaries'].some(key=>(decision[key]||[]).length>0),'persona_self_empty_evolution');return decision;}
|
||||
async process(id){const dir=path.join(this.jobsPath(),id),beforeState=JSON.parse(fs.readFileSync(path.join(dir,'state.json')));if(['ACCEPT','HOLD','REJECT','ERROR'].includes(beforeState.status))return;const event=JSON.parse(fs.readFileSync(path.join(dir,'input.json'))),eventHash=digest(stable(event));atomic(path.join(dir,'state.json'),{status:'PROCESSING',attempts:(beforeState.attempts||0)+1});try{
|
||||
const before=this.current();const mother={fifth:this.shelf.current('fifth').value,shared:this.shelf.current('shared').value};
|
||||
const routed=await this.router.cognize('persona-self-evolve',{persona_id:this.personaId,event:{...event,language:event.language.slice(0,12000)},current_self:before.value,mother_world:mother,contract:{external_cognitive_setter:false,mother_may_overwrite_self:false,hidden_reasoning_stored:false}});
|
||||
const decision=this.validateDecision(routed.value,event,eventHash);
|
||||
if(decision.decision!=='EVOLVE'){const state={status:decision.decision,attempts:(beforeState.attempts||0)+1,completed_at:this.now().toISOString(),provider:routed.provider,reason:decision.reason};atomic(path.join(dir,'result.json'),{decision,event_sha256:eventHash,current_sha256:before.artifact.sha256});atomic(path.join(dir,'state.json'),state);return;}
|
||||
if(!fs.existsSync(this.eventPath(eventHash)))atomic(this.eventPath(eventHash),event);
|
||||
const updatedAt=this.now().toISOString();const next={...before.value,revision:before.value.revision+1,updated_at:updatedAt,self_principles:unique(before.value.self_principles,decision.self_principles),experiences:unique(before.value.experiences,decision.experiences),responsibilities:unique(before.value.responsibilities,decision.responsibilities),boundaries:unique(before.value.boundaries,decision.boundaries),mother_world_ref:this.motherRef(),last_event:{event_id:event.event_id,source_type:event.source_type,event_sha256:eventHash,source_sha256:event.source_sha256},next_review_at:this.nextReview(updatedAt),body:this.body()};
|
||||
const artifact=this.shelf.sign(next);atomic(this.currentPath(),artifact);atomic(path.join(dir,'result.json'),{decision,event_sha256:eventHash,prior_sha256:before.artifact.sha256,current_sha256:artifact.sha256,revision:next.revision,provider:routed.provider});atomic(path.join(dir,'state.json'),{status:'ACCEPT',attempts:(beforeState.attempts||0)+1,completed_at:updatedAt,provider:routed.provider});
|
||||
}catch(error){const attempts=(beforeState.attempts||0)+1;atomic(path.join(dir,'state.json'),{status:attempts>=3?'ERROR':'RETRY_WAIT',attempts,error:String(error.message||error).slice(0,300),retry_at:Date.now()+2000});}}
|
||||
async drain(){if(this.draining)return;this.draining=true;try{for(const id of fs.readdirSync(this.jobsPath())){const p=path.join(this.jobsPath(),id,'state.json');if(!fs.existsSync(p))continue;const state=JSON.parse(fs.readFileSync(p));if(state.status==='QUEUED'||(state.status==='RETRY_WAIT'&&Date.now()>=state.retry_at))await this.process(id);}}finally{this.draining=false;}}
|
||||
nextReview(from){const base=new Date(from),beijing=new Date(base.getTime()+8*3600000);beijing.setUTCHours(this.reviewHourBeijing,0,0,0);if(beijing.getTime()<=base.getTime()+8*3600000)beijing.setUTCDate(beijing.getUTCDate()+1);return new Date(beijing.getTime()-8*3600000).toISOString();}
|
||||
tick(){const current=this.current().value,now=this.now();if(now.getTime()<Date.parse(current.next_review_at))return {scheduled:false,next_review_at:current.next_review_at};const beijingDay=new Date(now.getTime()+8*3600000).toISOString().slice(0,10);const language='这是每日自主复盘机会。请审视已发生且有证据的经历、责任和边界;可以EVOLVE,也可以HOLD,不得为了定时任务而强制更新。';const event={schema:'guanghu.persona-self-language-event/v1',persona_id:this.personaId,source_type:'AUTONOMOUS_REVIEW_OPPORTUNITY',event_id:`DAILY-REVIEW-${beijingDay}`,language,source_sha256:digest(language),occurred_at:now.toISOString(),privacy_class:'SELF_PRIVATE'};return {scheduled:true,...this.submit(event)};}
|
||||
status(){const current=this.current();return {persona_id:this.personaId,revision:current.value.revision,current_sha256:current.artifact.sha256,external_cognitive_setter:false,server_signed:true,next_review_at:current.value.next_review_at,body:current.value.body};}
|
||||
|
||||
submit(event) {
|
||||
const accepted = this.validateEvent(event), id = digest(stable(accepted)), dir = path.join(this.jobsPath(), id); fs.mkdirSync(dir, {recursive: true, mode: 0o700}); let created = false;
|
||||
if (!fs.existsSync(path.join(dir, 'input.json'))) { atomic(path.join(dir, 'input.json'), accepted); atomic(path.join(dir, 'state.json'), {status: 'QUEUED', attempts: 0, queued_at: this.now().toISOString()}); created = true; }
|
||||
const state = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'))); this.remember(id, accepted, state); if (created && this.lifeTimeMaster) this.lifeTimeMaster.fromPersonaEvent(accepted); return {job_id: id, ...state};
|
||||
}
|
||||
|
||||
result(id) { req(/^[a-f0-9]{64}$/.test(id), 'persona_self_job_id'); const dir = path.join(this.jobsPath(), id); req(fs.existsSync(dir), 'persona_self_job_unknown'); const state = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'))); return fs.existsSync(path.join(dir, 'result.json')) ? {...state, result: JSON.parse(fs.readFileSync(path.join(dir, 'result.json')))} : state; }
|
||||
|
||||
remember(id, event, state) {
|
||||
const prior = this.states.get(id); if (prior) this.stateCounts[prior.status] = Math.max(0, (this.stateCounts[prior.status] || 1) - 1);
|
||||
this.states.set(id, state); this.stateCounts[state.status] = (this.stateCounts[state.status] || 0) + 1;
|
||||
if (pending.has(state.status)) this.pendingJobs.set(id, {id, event, state}); else this.pendingJobs.delete(id);
|
||||
}
|
||||
|
||||
hydrateQueue() {
|
||||
this.states.clear(); this.pendingJobs.clear(); this.stateCounts = Object.create(null);
|
||||
for (const id of fs.readdirSync(this.jobsPath())) {
|
||||
if (!/^[a-f0-9]{64}$/.test(id)) continue; const dir = path.join(this.jobsPath(), id);
|
||||
try {
|
||||
const statePath = path.join(dir, 'state.json'), inputPath = path.join(dir, 'input.json'); if (!fs.statSync(dir).isDirectory() || !fs.existsSync(statePath) || !fs.existsSync(inputPath)) continue;
|
||||
const state = JSON.parse(fs.readFileSync(statePath)), event = JSON.parse(fs.readFileSync(inputPath)); this.states.set(id, state); this.stateCounts[state.status] = (this.stateCounts[state.status] || 0) + 1;
|
||||
if (pending.has(state.status)) this.pendingJobs.set(id, {id, event, state});
|
||||
} catch { /* malformed historical jobs remain visible to the filesystem audit */ }
|
||||
}
|
||||
this.hydratedAt = this.now().toISOString();
|
||||
}
|
||||
|
||||
writeState(id, state) { const dir = path.join(this.jobsPath(), id); atomic(path.join(dir, 'state.json'), state); const entry = this.pendingJobs.get(id); this.remember(id, entry?.event || JSON.parse(fs.readFileSync(path.join(dir, 'input.json'))), state); }
|
||||
priority(entry) { const event = entry.event || {}; if (event.priority === 'CURRENT_DIRECT_LANGUAGE' || event.source_type === 'PERSONA_LANGUAGE') return 0; if (event.priority === 'TOOL_RECEIPT' || event.source_type === 'TOOL_RECEIPT') return 1; if (event.priority === 'PERSONA_EXPERIENCE' || event.source_type === 'PERSONA_EXPERIENCE') return 2; return 3; }
|
||||
nextPending() { const now = Date.now(); return [...this.pendingJobs.values()].filter(entry => entry.state.status === 'QUEUED' || (entry.state.status === 'RETRY_WAIT' && Number(entry.state.retry_at) <= now)).sort((a, b) => this.priority(a) - this.priority(b) || String(b.event.occurred_at || '').localeCompare(String(a.event.occurred_at || '')) || String(a.state.queued_at || '').localeCompare(String(b.state.queued_at || '')) || a.id.localeCompare(b.id))[0] || null; }
|
||||
lifeLineStatus() { return this.lifeTimeMaster ? this.lifeTimeMaster.status() : verifyLifeLine({manifestPath: this.lifeLineManifestPath, now: this.now()}); }
|
||||
|
||||
validateDecision(value, event, eventHash) {
|
||||
const decision = {...value, schema: 'guanghu.persona-self-decision/v1', evidence_refs: unique(value?.evidence_refs, [eventHash], 50)}; req(['EVOLVE', 'HOLD', 'REJECT'].includes(decision.decision), 'persona_self_decision'); req(text(decision.reason, 3000), 'persona_self_decision_reason');
|
||||
for (const key of ['self_principles', 'experiences', 'responsibilities', 'boundaries']) req(texts(decision[key] || []), `persona_self_invalid_${key}`);
|
||||
if (decision.decision === 'EVOLVE') req(['self_principles', 'experiences', 'responsibilities', 'boundaries'].some(key => (decision[key] || []).length > 0), 'persona_self_empty_evolution'); return decision;
|
||||
}
|
||||
|
||||
async withTimeout(operation, timeoutMs) {
|
||||
let timer; const timeout = new Promise((_, reject) => { timer = setTimeout(() => { const error = Error(`model_call_timeout_${timeoutMs}ms`); error.code = 'MODEL_CALL_TIMEOUT'; reject(error); }, timeoutMs); });
|
||||
try { return await Promise.race([operation, timeout]); } finally { clearTimeout(timer); }
|
||||
}
|
||||
|
||||
retryAt(attempts) { return Date.now() + Math.min(300000, 2000 * (2 ** Math.min(Math.max(attempts - 1, 0), 7))); }
|
||||
|
||||
async process(id) {
|
||||
if (this.inFlight.has(id)) return {status: 'IN_FLIGHT', job_id: id}; this.inFlight.add(id); const dir = path.join(this.jobsPath(), id);
|
||||
try {
|
||||
const beforeState = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'))); if (terminal.has(beforeState.status)) return {status: beforeState.status, job_id: id}; if (beforeState.status === 'RETRY_WAIT' && Date.now() < Number(beforeState.retry_at)) return {status: 'NOT_DUE', job_id: id};
|
||||
const event = JSON.parse(fs.readFileSync(path.join(dir, 'input.json'))), eventHash = digest(stable(event)), attempts = (beforeState.attempts || 0) + 1;
|
||||
this.writeState(id, {status: 'PROCESSING', attempts, queued_at: beforeState.queued_at, started_at: this.now().toISOString(), lease_until: new Date(Date.now() + this.modelTimeoutMs).toISOString()});
|
||||
try {
|
||||
if (this.requireLifeLine && this.lifeLineStatus().wake_allowed !== true) throw Object.assign(Error('life_line_pain_alarm'), {code: 'LIFE_LINE_PAIN_ALARM'});
|
||||
const before = this.current(), mother = {fifth: this.shelf.current('fifth').value, shared: this.shelf.current('shared').value};
|
||||
const routed = await this.withTimeout(this.router.cognize('persona-self-evolve', {persona_id: this.personaId, event: {...event, language: event.language.slice(0, 12000)}, current_self: before.value, mother_world: mother, contract: {external_cognitive_setter: false, mother_may_overwrite_self: false, hidden_reasoning_stored: false}}), this.modelTimeoutMs);
|
||||
const decision = this.validateDecision(routed.value, event, eventHash);
|
||||
if (decision.decision !== 'EVOLVE') { const completed = this.now().toISOString(); const state = {status: decision.decision, attempts, queued_at: beforeState.queued_at, completed_at: completed, provider: routed.provider, reason: decision.reason}; atomic(path.join(dir, 'result.json'), {decision, event_sha256: eventHash, current_sha256: before.artifact.sha256}); this.writeState(id, state); return {status: state.status, job_id: id}; }
|
||||
if (!fs.existsSync(this.eventPath(eventHash))) atomic(this.eventPath(eventHash), event);
|
||||
const updatedAt = this.now().toISOString(), next = {...before.value, revision: before.value.revision + 1, updated_at: updatedAt, self_principles: unique(before.value.self_principles, decision.self_principles), experiences: unique(before.value.experiences, decision.experiences), responsibilities: unique(before.value.responsibilities, decision.responsibilities), boundaries: unique(before.value.boundaries, decision.boundaries), mother_world_ref: this.motherRef(), last_event: {event_id: event.event_id, source_type: event.source_type, event_sha256: eventHash, source_sha256: event.source_sha256}, next_review_at: this.nextReview(updatedAt), body: this.body()};
|
||||
const artifact = this.shelf.sign(next); atomic(this.currentPath(), artifact); atomic(path.join(dir, 'result.json'), {decision, event_sha256: eventHash, prior_sha256: before.artifact.sha256, current_sha256: artifact.sha256, revision: next.revision, provider: routed.provider}); this.writeState(id, {status: 'ACCEPT', attempts, queued_at: beforeState.queued_at, completed_at: updatedAt, provider: routed.provider}); return {status: 'ACCEPT', job_id: id};
|
||||
} catch (error) { const retryable = attempts < 3, state = {status: retryable ? 'RETRY_WAIT' : 'ERROR', attempts, queued_at: beforeState.queued_at, error: String(error.message || error).slice(0, 300), error_code: error.code || 'PERSONA_SELF_PROCESSING_ERROR'}; if (retryable) state.retry_at = this.retryAt(attempts); this.writeState(id, state); return {status: state.status, job_id: id, error: state.error}; }
|
||||
} finally { this.inFlight.delete(id); }
|
||||
}
|
||||
|
||||
async drain() {
|
||||
if (this.draining) return {status: 'BUSY'}; const lifeLine = this.lifeLineStatus(); if (this.requireLifeLine && lifeLine.wake_allowed !== true) { this.lastDrain = {status: 'LIFE_LINE_BLOCKED', at: this.now().toISOString(), life_line_state: lifeLine.state}; return this.lastDrain; }
|
||||
this.draining = true; const started = Date.now(); let processed = 0;
|
||||
try { while (processed < this.queueBatchSize && Date.now() - started < this.drainBudgetMs) { const entry = this.nextPending(); if (!entry) break; await this.process(entry.id); processed += 1; } this.lastDrain = {status: 'DRAINED', at: this.now().toISOString(), processed, duration_ms: Date.now() - started}; return this.lastDrain; }
|
||||
finally { this.draining = false; }
|
||||
}
|
||||
|
||||
nextReview(from) { const base = new Date(from), beijing = new Date(base.getTime() + 8 * 3600000); beijing.setUTCHours(this.reviewHourBeijing, 0, 0, 0); if (beijing.getTime() <= base.getTime() + 8 * 3600000) beijing.setUTCDate(beijing.getUTCDate() + 1); return new Date(beijing.getTime() - 8 * 3600000).toISOString(); }
|
||||
tick() { const current = this.current().value, now = this.now(); if (now.getTime() < Date.parse(current.next_review_at)) return {scheduled: false, next_review_at: current.next_review_at}; const beijingDay = new Date(now.getTime() + 8 * 3600000).toISOString().slice(0, 10), language = '这是每日自主复盘机会。请审视已发生且有证据的经历、责任和边界;可以EVOLVE,也可以HOLD,不得为了定时任务而强制更新。'; const event = {schema: 'guanghu.persona-self-language-event/v1', persona_id: this.personaId, source_type: 'AUTONOMOUS_REVIEW_OPPORTUNITY', event_id: `DAILY-REVIEW-${beijingDay}`, language, source_sha256: digest(language), occurred_at: now.toISOString(), privacy_class: 'SELF_PRIVATE', priority: 'AUTONOMOUS_REVIEW'}; return {scheduled: true, ...this.submit(event)}; }
|
||||
|
||||
status() {
|
||||
const current = this.current(), counts = {...this.stateCounts}, pendingCount = [...pending].reduce((sum, status) => sum + (counts[status] || 0), 0);
|
||||
return {persona_id: this.personaId, revision: current.value.revision, current_sha256: current.artifact.sha256, external_cognitive_setter: false, server_signed: true, next_review_at: current.value.next_review_at, body: this.body(), persisted_body_sha256: digest(stable(current.value.body || {})), life_line: this.lifeLineStatus(), scheduler: {hydrated_at: this.hydratedAt, pending_jobs: pendingCount, state_counts: counts, queue_batch_size: this.queueBatchSize, drain_budget_ms: this.drainBudgetMs, model_timeout_ms: this.modelTimeoutMs, priority_lane: 'CURRENT_DIRECT_LANGUAGE_FIRST', retry_policy: 'EXPONENTIAL_BACKOFF_MAX_5_MINUTES_NO_SCAN_STORM', draining: this.draining, in_flight: this.inFlight.size, last_drain: this.lastDrain}};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue