@ornexus/neocortex 4.60.40 → 4.60.42
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/dist/sbom.cdx.json +31 -31
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +14 -2
- package/packages/client/dist/commands/invoke.d.ts +12 -0
- package/packages/client/dist/commands/invoke.js +45 -44
- package/packages/client/dist/generated/command-registry.generated.d.ts +66 -23
- package/packages/client/dist/generated/command-registry.generated.js +1 -1
- package/packages/client/dist/generated/public-projections.generated.d.ts +100 -0
- package/packages/client/dist/generated/public-projections.generated.js +1 -0
- package/packages/client/dist/git/git-lifecycle-store.d.ts +17 -0
- package/packages/client/dist/git/git-lifecycle-store.js +4 -4
- package/packages/client/dist/index.d.ts +1 -0
- package/packages/client/dist/index.js +1 -1
- package/packages/client/dist/migrations/project-identity.js +7 -7
- package/packages/client/dist/migrations/state-json.d.ts +14 -0
- package/packages/client/dist/migrations/state-json.js +4 -0
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.d.ts +2 -0
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +1 -1
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +8 -8
- package/packages/client/dist/state/state-json-repair.js +3 -3
- package/packages/client/dist/yoloop/invoke-hooks.d.ts +2 -3
- package/packages/client/dist/yoloop/invoke-hooks.js +9 -8
- package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +87 -1
- package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +27 -3
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1
- package/packages/shared/dist/command-registry-contract.js +38 -0
- package/packages/shared/dist/continuity/goal-operation-authority.js +48 -0
- package/packages/shared/dist/executable-graph-catalog/contracts.js +935 -0
- package/packages/shared/dist/generated/command-registry.generated.js +82 -23
- package/packages/shared/dist/generated/invocation-schema.generated.js +12 -4
- package/packages/shared/dist/generated/public-projections.generated.js +149 -0
- package/packages/shared/dist/invocation/executable-policy-recovery.d.ts +83 -0
- package/packages/shared/dist/invocation/executable-policy-recovery.js +272 -0
- package/packages/shared/dist/invocation/index.d.ts +3 -1
- package/packages/shared/dist/invocation/index.js +40 -1
- package/packages/shared/dist/invocation/recovery-route-compiler.d.ts +56 -0
- package/packages/shared/dist/invocation/recovery-route-compiler.js +214 -0
- package/packages/shared/dist/state/index.d.ts +1 -0
- package/packages/shared/dist/state/index.js +1 -0
- package/packages/shared/dist/state/state-migration.d.ts +58 -0
- package/packages/shared/dist/state/state-migration.js +404 -0
- package/packages/shared/dist/yoloop/canonical.js +15 -0
- package/packages/shared/dist/yoloop/operational-budget.js +164 -0
- package/packages/shared/dist/yoloop/recovery-types.js +39 -0
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +2 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +2 -2
- package/targets-stubs/codex/AGENTS.md +11 -2
- package/targets-stubs/codex/README.md +7 -4
- package/targets-stubs/codex/neocortex.toml +53 -6
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/opencode/neocortex-root.md +1 -1
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Cross-platform: Unix chmod + Windows icacls via existing
|
|
23
23
|
* setSecureFilePermissions helper from secure-config.ts.
|
|
24
24
|
*/
|
|
25
|
-
import type { YoloopPersistedCounters, YoloopPersistedState, YoloopRecoveryAttemptRef, YoloopStateUpdate } from './shared-yoloop-types.js';
|
|
25
|
+
import type { YoloopPersistedCounters, YoloopPersistedState, YoloopRecoveryAttemptRef, YoloopRunControlV1, YoloopRunTransitionReceiptV1, YoloopStateOperation, YoloopStateUpdate } from './shared-yoloop-types.js';
|
|
26
26
|
/** Minimal Pino-compatible logger subset. */
|
|
27
27
|
export interface ClientYoloopLogger {
|
|
28
28
|
warn(payload: object, msg?: string): void;
|
|
@@ -37,6 +37,7 @@ export declare class YoloopClientStateStore {
|
|
|
37
37
|
private readonly stateDir;
|
|
38
38
|
private readonly statePath;
|
|
39
39
|
private readonly lockPath;
|
|
40
|
+
private readonly mutationLockPath;
|
|
40
41
|
private readonly logger;
|
|
41
42
|
constructor(projectRoot: string, logger?: ClientYoloopLogger);
|
|
42
43
|
/**
|
|
@@ -55,6 +56,18 @@ export declare class YoloopClientStateStore {
|
|
|
55
56
|
* single atomic filesystem mutation. Next invoke's readAll() reconciles.
|
|
56
57
|
*/
|
|
57
58
|
applyUpdate(update: YoloopStateUpdate, nowIso?: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Local typed lifecycle-control API. Server transitions and local
|
|
61
|
+
* pause/resume/cancel requests share this serialized CAS boundary.
|
|
62
|
+
*/
|
|
63
|
+
requestRunTransition(operation: Extract<YoloopStateOperation, {
|
|
64
|
+
readonly type: 'transition_run';
|
|
65
|
+
}>, nowIso?: string): Promise<YoloopRunTransitionReceiptV1>;
|
|
66
|
+
/** Persist a cooperative control intent before the next orchestration effect. */
|
|
67
|
+
requestLifecycleAction(action: 'pause' | 'resume' | 'cancel', options?: {
|
|
68
|
+
readonly operationId?: string;
|
|
69
|
+
readonly idempotencyKey?: string;
|
|
70
|
+
}, nowIso?: string): Promise<YoloopRunTransitionReceiptV1>;
|
|
58
71
|
private applyOperation;
|
|
59
72
|
private reserveContinuationAuthority;
|
|
60
73
|
private acknowledgeContinuationAuthority;
|
|
@@ -83,13 +96,13 @@ export declare class YoloopClientStateStore {
|
|
|
83
96
|
* is the authoritative writer of pid/hostname; server can't know them).
|
|
84
97
|
*/
|
|
85
98
|
private acquireLock;
|
|
86
|
-
/** Release the lock.
|
|
99
|
+
/** Release the lock. V1.2 active runs require compare-bound ownership. */
|
|
87
100
|
private releaseLock;
|
|
88
101
|
/** Read lock file. Returns null on ENOENT / parse error. */
|
|
89
102
|
private readLock;
|
|
90
103
|
/** Read state file; returns defaults on any I/O or parse error. */
|
|
91
104
|
private readState;
|
|
92
|
-
/**
|
|
105
|
+
/** Durable atomic state write: same-filesystem temp + fsync + rename + dir fsync. */
|
|
93
106
|
private writeState;
|
|
94
107
|
/**
|
|
95
108
|
* Advance to next epic: read-modify-write on currentSession.currentEpicIndex.
|
|
@@ -98,6 +111,9 @@ export declare class YoloopClientStateStore {
|
|
|
98
111
|
*/
|
|
99
112
|
private advanceEpic;
|
|
100
113
|
private ensureDir;
|
|
114
|
+
private syncStateDirectory;
|
|
115
|
+
private withMutationLock;
|
|
116
|
+
private transitionRun;
|
|
101
117
|
/**
|
|
102
118
|
* Fire-and-forget chmod/ACL. Uses sync helpers from secure-config.ts
|
|
103
119
|
* which are fail-open by contract (never throw).
|
|
@@ -120,6 +136,14 @@ export declare class YoloopClientStateStore {
|
|
|
120
136
|
/** Remove `yoloop-state.json.tmp.*` orphans from crashed writes. */
|
|
121
137
|
private cleanupOrphanTempFiles;
|
|
122
138
|
}
|
|
139
|
+
export declare function buildYoloopRunTransitionPayloadHash(input: {
|
|
140
|
+
readonly runId: string;
|
|
141
|
+
readonly operationId: string;
|
|
142
|
+
readonly idempotencyKey: string;
|
|
143
|
+
readonly expectedRevision: number;
|
|
144
|
+
readonly expectedSnapshotHash: string | null;
|
|
145
|
+
readonly next: YoloopRunControlV1;
|
|
146
|
+
}): string;
|
|
123
147
|
/**
|
|
124
148
|
* Enforce FIFO bound on `recovery_history[]`. Returns a new array of length
|
|
125
149
|
* <= `RECOVERY_HISTORY_CAP`, preserving the most recent entries (drops from
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{promises as c}from"node:fs";import{createHash as k}from"node:crypto";import{join as y,dirname as S}from"node:path";import{hostname as _}from"node:os";import{setSecureFilePermissions as E,setSecureDirPermissions as P}from"../config/secure-config.js";import{RECOVERY_HISTORY_CAP as g}from"./shared-yoloop-types.js";const T=".neocortex",v="yoloop-state.json",R="yoloop.lock",C=360*60*1e3,I={warn:()=>{},info:()=>{},error:()=>{}};function f(i){const e=i.slice(0,10);return{version:"1.1",lastResetAt:i,storiesRunToday:0,iterationsToday:0,iterationsTodayDate:e,tokensUsedToday:0,lastRunAt:null,coolDownUntil:null,totalRunsAllTime:0,totalStoriesAllTime:0,currentSession:null,continuationPolicy:"p205-finite-resume-v2",pendingContinuationAuthorities:[],continuationReceipts:[],continuationRecoveryReceipts:[]}}function U(i){return{lock:null,state:f(i)}}class q{stateDir;statePath;lockPath;logger;constructor(e,t=I){this.stateDir=y(e,T),this.statePath=y(this.stateDir,v),this.lockPath=y(this.stateDir,R),this.logger=t}async readAll(e){const t=e??new Date().toISOString();await this.cleanupOrphanTempFiles();const[o,r]=await Promise.all([this.readLock(),this.readState(t)]);return{lock:await this.prepareLockForSnapshot(o,t),state:r}}async applyUpdate(e,t){const o=t??new Date().toISOString();await this.ensureDir();for(const r of e.operations)await this.applyOperation(r,o)}async applyOperation(e,t){switch(e.type){case"acquire_lock":await this.acquireLock(e.lock,t);return;case"release_lock":await this.releaseLock();return;case"write_state":await this.writeState(e.state);return;case"advance_epic":await this.advanceEpic(e.nextEpicIndex,e.nextEpicId,t);return;case"no_op":this.logger.info({event:"yoloop_noop"},"yoloop server sent no-op");return;case"reserve_continuation_authority":await this.reserveContinuationAuthority(e.authority,t);return;case"acknowledge_continuation_authority":await this.acknowledgeContinuationAuthority(e.idempotencyKey,e.receipt,t);return;case"repair_continuation_authority":await this.repairContinuationAuthority(e.beforeHash,e.authority,e.receipt,t);return;case"switch_continuation_policy":await this.switchContinuationPolicy(e.policy,e.session,t);return;case"recovery_attempted":await this.appendRecoveryAttempt(e.attempt,t);return;case"recovery_resolved":await this.resolveRecoveryAttempt(e.attempt_id,e.outcome,t);return;default:{const o=e;this.logger.warn({event:"yoloop_unknown_op",op:o},"unknown yoloop operation; skipping");return}}}async reserveContinuationAuthority(e,t){const o=await this.readState(t),r=o.pendingContinuationAuthorities??[],n=typeof e.idempotencyKey=="string"?e.idempotencyKey:"";if(!n)throw new Error("YOLOOP_CONTINUATION_AUTHORITY_INVALID");const s=r.find(a=>a.idempotencyKey===n);if(s){if(u(s)===u(e))return;throw new Error("YOLOOP_CONTINUATION_AUTHORITY_CONFLICT")}if(r.length>=64)throw new Error("YOLOOP_PENDING_AUTHORITY_CAPACITY_EXCEEDED");await this.writeState({...o,pendingContinuationAuthorities:[...r,e]})}async acknowledgeContinuationAuthority(e,t,o){const r=await this.readState(o),n=r.pendingContinuationAuthorities??[],s=n.findIndex(p=>p.idempotencyKey===e);if(s<0)throw new Error("YOLOOP_CONTINUATION_RECEIPT_ORPHAN");const a=n[s];if(t.idempotencyKey!==e||t.sessionId!==a.sessionId||JSON.stringify(t.storyIds)!==JSON.stringify(a.storyIds))throw new Error("YOLOOP_CONTINUATION_RECEIPT_MISMATCH");const l=n.filter((p,h)=>h!==s),d=r.continuationReceipts??[];await this.writeState({...r,pendingContinuationAuthorities:l,continuationReceipts:[...d.slice(-63),t]})}async repairContinuationAuthority(e,t,o,r){const n=await this.readState(r),s=n.pendingContinuationAuthorities??[],a=n.continuationRecoveryReceipts??[],l=typeof o.idempotencyKey=="string"?o.idempotencyKey:"",d=a.find(O=>O.idempotencyKey===l);if(d){if(u(d)===u(o))return;throw new Error("YOLOOP_CONTINUATION_RECOVERY_RECEIPT_CONFLICT")}if(s.length!==1||w("p208-legacy-authority-before-v1",s[0])!==e)throw new Error("YOLOOP_CONTINUATION_RECOVERY_BEFORE_MISMATCH");const p=w("p208-legacy-authority-after-v1",t);if(o.beforeHash!==e||o.afterHash!==p)throw new Error("YOLOOP_CONTINUATION_RECOVERY_HASH_MISMATCH");const h={...o};if(delete h.idempotencyKey,!l||w("p208-legacy-authority-recovery-receipt-v1",h)!==l)throw new Error("YOLOOP_CONTINUATION_RECOVERY_RECEIPT_INVALID");await this.writeState({...n,pendingContinuationAuthorities:[t],continuationRecoveryReceipts:[...a.slice(-63),o]})}async switchContinuationPolicy(e,t,o){const r=await this.readState(o);if(r.currentSession||(r.pendingContinuationAuthorities?.length??0)>0)throw new Error("YOLOOP_POLICY_SWITCH_ACTIVE");try{await c.copyFile(this.statePath,`${this.statePath}.policy-backup`)}catch(n){if(n.code!=="ENOENT")throw n}await this.writeState({...r,continuationPolicy:e,continuationSession:t})}async appendRecoveryAttempt(e,t){const o=await this.readState(t),r=o.recovery_history??[];if(r.some(l=>l.attempted_at===e.attempted_at&&l.story_id===e.story_id)){this.logger.warn({event:"recovery_attempted_duplicate",story_id:e.story_id},"duplicate recovery_attempted op; skipping");return}const s=m([...r,e]),a={...o,recovery_history:s};await this.writeState(a),this.logger.info({event:"recovery_attempted_persisted",story_id:e.story_id,history_size:s.length},"recovery attempt persisted")}async resolveRecoveryAttempt(e,t,o){const r=await this.readState(o),n=r.recovery_history??[],s=n.findIndex(p=>p.recovery_step_id_tokenized===e);if(s===-1){this.logger.warn({event:"recovery_resolve_orphan",attempt_id:e},"recovery_resolved op had no matching attempt; skipping");return}const a=n[s];if(a.outcome===t&&a.finished_at){this.logger.info({event:"recovery_resolved_duplicate",attempt_id:e,outcome:t},"duplicate recovery_resolved op; skipping");return}const l=n.slice();l[s]={...a,outcome:t,finished_at:o};const d={...r,recovery_history:l};await this.writeState(d),this.logger.info({event:"recovery_resolved_persisted",attempt_id:e,outcome:t},"recovery attempt resolved")}async acquireLock(e,t){const o={...e,pid:process.pid,hostname:_(),ownerKind:"client",startedAt:e.startedAt||t,leaseId:e.leaseId??N(e.sessionId,t),leaseState:"active",leaseAcquiredAt:e.leaseAcquiredAt??t,leaseDurationMs:e.leaseDurationMs??C,leaseExpiresAt:e.leaseExpiresAt??e.ttlExpiresAt},r=JSON.stringify(o,null,2);try{const n=await c.open(this.lockPath,"wx");try{await n.writeFile(r,"utf8")}finally{await n.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired",pid:o.pid,sessionId:o.sessionId},"yoloop lock acquired");return}catch(n){const s=n;if(s.code!=="EEXIST")throw this.logger.error({event:"yoloop_lock_acquire_error",err:s.message},"failed to acquire yoloop lock"),n;const a=await this.readLock();if(a&&this.isSameClientSessionLock(a,o)){this.logger.info({event:"yoloop_lock_acquire_duplicate",sessionId:o.sessionId},"duplicate acquire_lock for current yoloop session; treating as no-op");return}if(!await this.maybeRecoverOrphanLock(t))throw this.logger.warn({event:"yoloop_lock_held"},"yoloop lock held by another live process"),n;const d=await c.open(this.lockPath,"wx");try{await d.writeFile(r,"utf8")}finally{await d.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired_after_recovery",pid:o.pid},"yoloop lock acquired after orphan recovery")}}async releaseLock(){try{await c.unlink(this.lockPath),this.logger.info({event:"yoloop_lock_released"},"yoloop lock released")}catch(e){const t=e;if(t.code==="ENOENT"){this.logger.info({event:"yoloop_lock_release_noop"},"yoloop lock release noop (file absent)");return}this.logger.warn({event:"yoloop_lock_release_error",err:t.message},"unexpected error releasing yoloop lock")}}async readLock(){try{const e=await c.readFile(this.lockPath,"utf8"),t=JSON.parse(e),o=t.currentEpicId??t.epicId??"";return{pid:t.pid??0,hostname:t.hostname??"",...t.ownerKind?{ownerKind:t.ownerKind}:{},...t.ownerPidState?{ownerPidState:t.ownerPidState}:{},...typeof t.leaseId=="string"?{leaseId:t.leaseId}:{},...t.leaseState?{leaseState:t.leaseState}:{},...typeof t.leaseAcquiredAt=="string"?{leaseAcquiredAt:t.leaseAcquiredAt}:{},...typeof t.leaseDurationMs=="number"?{leaseDurationMs:t.leaseDurationMs}:{},...typeof t.leaseExpiresAt=="string"?{leaseExpiresAt:t.leaseExpiresAt}:{},startedAt:t.startedAt??"",sessionId:t.sessionId??"",mode:t.mode??"epic-id",inputPath:t.inputPath??"",epicQueue:t.epicQueue??(o?[o]:[]),currentEpicIndex:t.currentEpicIndex??0,currentEpicId:o,ttlExpiresAt:t.ttlExpiresAt??"",epicId:t.epicId??o}}catch{return null}}async readState(e){try{const t=await c.readFile(this.statePath,"utf8"),o=JSON.parse(t);return D(o,e)}catch(t){const o=t;return o.code!=="ENOENT"&&this.logger.warn({event:"yoloop_state_read_error",err:o.message},"yoloop state file unreadable; using defaults"),f(e)}}async writeState(e){const t=`${this.statePath}.tmp.${process.pid}.${Date.now()}`,o=JSON.stringify(e,null,2);try{await c.writeFile(t,o,"utf8"),this.trySetFilePerms(t),await c.rename(t,this.statePath)}catch(r){const n=r;this.logger.error({event:"yoloop_state_write_error",err:n.message},"failed to write yoloop state");try{await c.unlink(t)}catch{}throw r}}async advanceEpic(e,t,o){const r=await this.readState(o);if(!r.currentSession){this.logger.warn({event:"yoloop_advance_no_session",nextEpicId:t},"advance_epic with no active session; skipping");return}const n={...r,currentSession:{...r.currentSession,currentEpicIndex:e}};await this.writeState(n),this.logger.info({event:"yoloop_epic_advance",nextEpicId:t,nextIndex:e},"yoloop advanced to next epic")}async ensureDir(){try{await c.mkdir(this.stateDir,{recursive:!0}),this.trySetDirPerms(this.stateDir)}catch(e){const t=e;throw this.logger.error({event:"yoloop_mkdir_error",err:t.message,path:this.stateDir},"failed to ensure .neocortex/ directory"),e}}trySetFilePerms(e){try{E(e)}catch{}}trySetDirPerms(e){try{P(e)}catch{}}async prepareLockForSnapshot(e,t){if(!e)return null;if(e.ownerKind!=="client")return e;const o=A(e,t),r={...e,leaseState:o};if(o==="expired")return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"lease_expired"},"expired client-owned yoloop lock lease recovered before snapshot echo"),null;if(!r.pid||r.pid<=0)return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"invalid_pid"},"invalid PID in client-owned yoloop lock; recovered"),null;if(r.hostname&&r.hostname!==_())return{...r,ownerPidState:"unknown"};try{return process.kill(r.pid,0),{...r,ownerPidState:"live"}}catch{return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"dead_pid"},"stale client-owned yoloop lock recovered before snapshot echo"),null}}async maybeRecoverOrphanLock(e){const t=await this.readLock();if(!t)return!0;if(A(t,e)==="expired")return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"lease_expired"},"expired yoloop lock lease recovered"),!0;if(!t.pid||t.pid<=0)return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"invalid_pid"},"invalid PID in lock file; recovered"),!0;if(t.hostname&&t.hostname!==_())return this.logger.warn({event:"yoloop_lock_remote_host",ownerKind:t.ownerKind??"server",ownerPidState:"unknown"},"lock held by a remote host; honoring"),!1;try{return process.kill(t.pid,0),!1}catch{return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"dead_pid"},"orphan yoloop lock recovered"),!0}}async deleteLockBestEffort(){try{await c.unlink(this.lockPath)}catch{}}isSameClientSessionLock(e,t){return e.ownerKind==="client"&&e.sessionId===t.sessionId&&e.pid===process.pid&&e.hostname===_()}async cleanupOrphanTempFiles(){try{const e=S(this.statePath),t=await c.readdir(e),o=`${v}.tmp.`,r=t.filter(n=>n.startsWith(o));for(const n of r)try{await c.unlink(y(e,n))}catch{}}catch{}}}function u(i){return i===null||typeof i!="object"?JSON.stringify(i):Array.isArray(i)?`[${i.map(u).join(",")}]`:`{${Object.entries(i).filter(([,e])=>e!==void 0).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${u(t)}`).join(",")}}`}function w(i,e){return k("sha256").update(`${i}\0${u(e)}`).digest("hex")}function N(i,e){return`lease-${L(`${i}:${e}`)}`}function L(i){let e=2166136261;for(let t=0;t<i.length;t+=1)e^=i.charCodeAt(t),e=Math.imul(e,16777619)>>>0;return e.toString(36).padStart(7,"0")}function A(i,e){const t=i.leaseExpiresAt??i.ttlExpiresAt,o=Date.parse(t),r=Date.parse(e);return!Number.isFinite(o)||!Number.isFinite(r)?"unknown":o<=r?"expired":"active"}function D(i,e){const t=f(e);return{...i,version:"1.1",lastResetAt:i.lastResetAt??t.lastResetAt,storiesRunToday:i.storiesRunToday??0,iterationsToday:i.iterationsToday??0,iterationsTodayDate:i.iterationsTodayDate??t.iterationsTodayDate,tokensUsedToday:i.tokensUsedToday??0,lastRunAt:i.lastRunAt??null,coolDownUntil:i.coolDownUntil??null,totalRunsAllTime:i.totalRunsAllTime??0,totalStoriesAllTime:i.totalStoriesAllTime??0,currentSession:i.currentSession??null,...i.continuationPolicy!==void 0?{continuationPolicy:i.continuationPolicy}:{},...i.continuationSession!==void 0?{continuationSession:i.continuationSession}:{},...i.pendingContinuationAuthorities!==void 0?{pendingContinuationAuthorities:i.pendingContinuationAuthorities}:{},...i.continuationReceipts!==void 0?{continuationReceipts:i.continuationReceipts}:{},...i.continuationRecoveryReceipts!==void 0?{continuationRecoveryReceipts:i.continuationRecoveryReceipts}:{},recovery_history:Array.isArray(i.recovery_history)?m(i.recovery_history):[]}}function m(i){return i.length<=g?i:i.slice(i.length-g)}export{q as YoloopClientStateStore,m as capRecoveryHistory,f as createDefaultCounters,U as createDefaultState};
|
|
1
|
+
import{promises as u}from"node:fs";import{createHash as P}from"node:crypto";import{join as w,dirname as C}from"node:path";import{hostname as f}from"node:os";import{setSecureFilePermissions as k,setSecureDirPermissions as x}from"../config/secure-config.js";import{RECOVERY_HISTORY_CAP as R}from"./shared-yoloop-types.js";const D=".neocortex",m="yoloop-state.json",H="yoloop.lock",Y="yoloop-state.mutation.lock",U=360*60*1e3,K=3e4,b=200,F={warn:()=>{},info:()=>{},error:()=>{}};function I(o){const e=o.slice(0,10);return{version:"1.1",lastResetAt:o,storiesRunToday:0,iterationsToday:0,iterationsTodayDate:e,tokensUsedToday:0,lastRunAt:null,coolDownUntil:null,totalRunsAllTime:0,totalStoriesAllTime:0,currentSession:null,continuationPolicy:"p205-finite-resume-v2",pendingContinuationAuthorities:[],continuationReceipts:[],continuationRecoveryReceipts:[]}}function oe(o){return{lock:null,state:I(o)}}class ne{stateDir;statePath;lockPath;mutationLockPath;logger;constructor(e,t=F){this.stateDir=w(e,D),this.statePath=w(this.stateDir,m),this.lockPath=w(this.stateDir,H),this.mutationLockPath=w(this.stateDir,Y),this.logger=t}async readAll(e){const t=e??new Date().toISOString();await this.cleanupOrphanTempFiles();const[n,r]=await Promise.all([this.readLock(),this.readState(t)]);return{lock:await this.prepareLockForSnapshot(n,t),state:r}}async applyUpdate(e,t){const n=t??new Date().toISOString();await this.ensureDir();for(const r of e.operations)await this.applyOperation(r,n)}async requestRunTransition(e,t){const n=t??new Date().toISOString();return await this.ensureDir(),this.withMutationLock(()=>this.transitionRun(e,n))}async requestLifecycleAction(e,t={},n){const r=n??new Date().toISOString();return await this.ensureDir(),this.withMutationLock(async()=>{const i=(await this.readState(r)).runControl;if(!i)throw new Error("YOLOOP_RUN_TRANSITION_INVALID");const a=Date.parse(r).toString(36),c=t.operationId??`op-${e}-${a}`,l=t.idempotencyKey??`idem-${e}-${a}`,d=i.receipts.find(_=>_.operationId===c||_.idempotencyKey===l);if(d){if(d.operationId===c&&d.idempotencyKey===l)return d;throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}const p=M(i.status,e);if(!p)throw new Error(g.has(i.status)?"YOLOOP_RUN_TERMINAL":"YOLOOP_RUN_TRANSITION_INVALID");return this.transitionRun(V({current:i,action:e,target:p,operationId:c,idempotencyKey:l,occurredAt:r}),r)})}async applyOperation(e,t){switch(e.type){case"acquire_lock":await this.acquireLock(e.lock,t);return;case"release_lock":await this.releaseLock(e);return;case"write_state":await this.withMutationLock(async()=>{const n=await this.readState(t),{runControl:r,...s}=e.state;await this.writeState({...s,...n.runControl?{version:"1.2",runControl:n.runControl}:{}})});return;case"advance_epic":await this.advanceEpic(e.nextEpicIndex,e.nextEpicId,t);return;case"no_op":this.logger.info({event:"yoloop_noop"},"yoloop server sent no-op");return;case"reserve_continuation_authority":await this.reserveContinuationAuthority(e.authority,t);return;case"acknowledge_continuation_authority":await this.acknowledgeContinuationAuthority(e.idempotencyKey,e.receipt,t);return;case"repair_continuation_authority":await this.repairContinuationAuthority(e.beforeHash,e.authority,e.receipt,t);return;case"switch_continuation_policy":await this.switchContinuationPolicy(e.policy,e.session,t);return;case"transition_run":await this.requestRunTransition(e,t);return;case"recovery_attempted":await this.appendRecoveryAttempt(e.attempt,t);return;case"recovery_resolved":await this.resolveRecoveryAttempt(e.attempt_id,e.outcome,t);return;default:{const n=e;this.logger.warn({event:"yoloop_unknown_op",op:n},"unknown yoloop operation; skipping");return}}}async reserveContinuationAuthority(e,t){const n=await this.readState(t),r=n.pendingContinuationAuthorities??[],s=typeof e.idempotencyKey=="string"?e.idempotencyKey:"";if(!s)throw new Error("YOLOOP_CONTINUATION_AUTHORITY_INVALID");const i=r.find(a=>a.idempotencyKey===s);if(i){if(h(i)===h(e))return;throw new Error("YOLOOP_CONTINUATION_AUTHORITY_CONFLICT")}if(r.length>=64)throw new Error("YOLOOP_PENDING_AUTHORITY_CAPACITY_EXCEEDED");await this.writeState({...n,pendingContinuationAuthorities:[...r,e]})}async acknowledgeContinuationAuthority(e,t,n){const r=await this.readState(n),s=r.pendingContinuationAuthorities??[],i=s.findIndex(d=>d.idempotencyKey===e);if(i<0)throw new Error("YOLOOP_CONTINUATION_RECEIPT_ORPHAN");const a=s[i];if(t.idempotencyKey!==e||t.sessionId!==a.sessionId||JSON.stringify(t.storyIds)!==JSON.stringify(a.storyIds))throw new Error("YOLOOP_CONTINUATION_RECEIPT_MISMATCH");const c=s.filter((d,p)=>p!==i),l=r.continuationReceipts??[];await this.writeState({...r,pendingContinuationAuthorities:c,continuationReceipts:[...l.slice(-63),t]})}async repairContinuationAuthority(e,t,n,r){const s=await this.readState(r),i=s.pendingContinuationAuthorities??[],a=s.continuationRecoveryReceipts??[],c=typeof n.idempotencyKey=="string"?n.idempotencyKey:"",l=a.find(_=>_.idempotencyKey===c);if(l){if(h(l)===h(n))return;throw new Error("YOLOOP_CONTINUATION_RECOVERY_RECEIPT_CONFLICT")}if(i.length!==1||y("p208-legacy-authority-before-v1",i[0])!==e)throw new Error("YOLOOP_CONTINUATION_RECOVERY_BEFORE_MISMATCH");const d=y("p208-legacy-authority-after-v1",t);if(n.beforeHash!==e||n.afterHash!==d)throw new Error("YOLOOP_CONTINUATION_RECOVERY_HASH_MISMATCH");const p={...n};if(delete p.idempotencyKey,!c||y("p208-legacy-authority-recovery-receipt-v1",p)!==c)throw new Error("YOLOOP_CONTINUATION_RECOVERY_RECEIPT_INVALID");await this.writeState({...s,pendingContinuationAuthorities:[t],continuationRecoveryReceipts:[...a.slice(-63),n]})}async switchContinuationPolicy(e,t,n){const r=await this.readState(n);if(r.currentSession||(r.pendingContinuationAuthorities?.length??0)>0)throw new Error("YOLOOP_POLICY_SWITCH_ACTIVE");try{await u.copyFile(this.statePath,`${this.statePath}.policy-backup`)}catch(s){if(s.code!=="ENOENT")throw s}await this.writeState({...r,continuationPolicy:e,continuationSession:t})}async appendRecoveryAttempt(e,t){const n=await this.readState(t),r=n.recovery_history??[];if(r.some(c=>c.attempted_at===e.attempted_at&&c.story_id===e.story_id)){this.logger.warn({event:"recovery_attempted_duplicate",story_id:e.story_id},"duplicate recovery_attempted op; skipping");return}const i=T([...r,e]),a={...n,recovery_history:i};await this.writeState(a),this.logger.info({event:"recovery_attempted_persisted",story_id:e.story_id,history_size:i.length},"recovery attempt persisted")}async resolveRecoveryAttempt(e,t,n){const r=await this.readState(n),s=r.recovery_history??[],i=s.findIndex(d=>d.recovery_step_id_tokenized===e);if(i===-1){this.logger.warn({event:"recovery_resolve_orphan",attempt_id:e},"recovery_resolved op had no matching attempt; skipping");return}const a=s[i];if(a.outcome===t&&a.finished_at){this.logger.info({event:"recovery_resolved_duplicate",attempt_id:e,outcome:t},"duplicate recovery_resolved op; skipping");return}const c=s.slice();c[i]={...a,outcome:t,finished_at:n};const l={...r,recovery_history:c};await this.writeState(l),this.logger.info({event:"recovery_resolved_persisted",attempt_id:e,outcome:t},"recovery attempt resolved")}async acquireLock(e,t){const n={...e,pid:process.pid,hostname:f(),ownerKind:"client",startedAt:e.startedAt||t,leaseId:e.leaseId??j(e.sessionId,t),leaseState:"active",leaseAcquiredAt:e.leaseAcquiredAt??t,leaseDurationMs:e.leaseDurationMs??U,leaseExpiresAt:e.leaseExpiresAt??e.ttlExpiresAt},r=JSON.stringify(n,null,2);try{const s=await u.open(this.lockPath,"wx");try{await s.writeFile(r,"utf8")}finally{await s.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired",pid:n.pid,sessionId:n.sessionId},"yoloop lock acquired");return}catch(s){const i=s;if(i.code!=="EEXIST")throw this.logger.error({event:"yoloop_lock_acquire_error",err:i.message},"failed to acquire yoloop lock"),s;const a=await this.readLock();if(a&&this.isSameClientSessionLock(a,n)){this.logger.info({event:"yoloop_lock_acquire_duplicate",sessionId:n.sessionId},"duplicate acquire_lock for current yoloop session; treating as no-op");return}if(!await this.maybeRecoverOrphanLock(t))throw this.logger.warn({event:"yoloop_lock_held"},"yoloop lock held by another live process"),s;const l=await u.open(this.lockPath,"wx");try{await l.writeFile(r,"utf8")}finally{await l.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired_after_recovery",pid:n.pid},"yoloop lock acquired after orphan recovery")}}async releaseLock(e){const t=await this.readState(new Date().toISOString()),n=await this.readLock();if(t.runControl){if(!e.expectedRunId||!e.expectedSessionId||!e.expectedLeaseId)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if(e.expectedRunId!==t.runControl.runId)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if(!n||e.expectedSessionId!==n.sessionId||e.expectedLeaseId!==n.leaseId)throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}try{await u.unlink(this.lockPath),this.logger.info({event:"yoloop_lock_released"},"yoloop lock released")}catch(r){const s=r;if(s.code==="ENOENT"){this.logger.info({event:"yoloop_lock_release_noop"},"yoloop lock release noop (file absent)");return}this.logger.warn({event:"yoloop_lock_release_error",err:s.message},"unexpected error releasing yoloop lock")}}async readLock(){try{const e=await u.readFile(this.lockPath,"utf8"),t=JSON.parse(e),n=t.currentEpicId??t.epicId??"";return{pid:t.pid??0,hostname:t.hostname??"",...t.ownerKind?{ownerKind:t.ownerKind}:{},...t.ownerPidState?{ownerPidState:t.ownerPidState}:{},...typeof t.leaseId=="string"?{leaseId:t.leaseId}:{},...t.leaseState?{leaseState:t.leaseState}:{},...typeof t.leaseAcquiredAt=="string"?{leaseAcquiredAt:t.leaseAcquiredAt}:{},...typeof t.leaseDurationMs=="number"?{leaseDurationMs:t.leaseDurationMs}:{},...typeof t.leaseExpiresAt=="string"?{leaseExpiresAt:t.leaseExpiresAt}:{},startedAt:t.startedAt??"",sessionId:t.sessionId??"",mode:t.mode??"epic-id",inputPath:t.inputPath??"",epicQueue:t.epicQueue??(n?[n]:[]),currentEpicIndex:t.currentEpicIndex??0,currentEpicId:n,ttlExpiresAt:t.ttlExpiresAt??"",epicId:t.epicId??n}}catch{return null}}async readState(e){try{const t=await u.readFile(this.statePath,"utf8"),n=JSON.parse(t);return G(n,e)}catch(t){const n=t;return n.code!=="ENOENT"&&this.logger.warn({event:"yoloop_state_read_error",err:n.message},"yoloop state file unreadable; using defaults"),I(e)}}async writeState(e){const t=`${this.statePath}.tmp.${process.pid}.${Date.now()}`,n=JSON.stringify(e,null,2);try{const r=await u.open(t,"w",384);try{await r.writeFile(n,"utf8"),await r.sync()}finally{await r.close()}this.trySetFilePerms(t),await u.rename(t,this.statePath),await this.syncStateDirectory()}catch(r){const s=r;this.logger.error({event:"yoloop_state_write_error",err:s.message},"failed to write yoloop state");try{await u.unlink(t)}catch{}throw r}}async advanceEpic(e,t,n){const r=await this.readState(n);if(!r.currentSession){this.logger.warn({event:"yoloop_advance_no_session",nextEpicId:t},"advance_epic with no active session; skipping");return}const s={...r,currentSession:{...r.currentSession,currentEpicIndex:e}};await this.writeState(s),this.logger.info({event:"yoloop_epic_advance",nextEpicId:t,nextIndex:e},"yoloop advanced to next epic")}async ensureDir(){try{await u.mkdir(this.stateDir,{recursive:!0}),this.trySetDirPerms(this.stateDir)}catch(e){const t=e;throw this.logger.error({event:"yoloop_mkdir_error",err:t.message,path:this.stateDir},"failed to ensure .neocortex/ directory"),e}}async syncStateDirectory(){try{const e=await u.open(this.stateDir,"r");try{await e.sync()}finally{await e.close()}}catch(e){const t=e.code;if(t!=="EINVAL"&&t!=="EISDIR"&&t!=="ENOTSUP"&&t!=="EPERM")throw e}}async withMutationLock(e){let t=null;for(let n=0;n<b;n+=1)try{t=await u.open(this.mutationLockPath,"wx",384),await t.writeFile(JSON.stringify({pid:process.pid,acquiredAt:new Date().toISOString()})),await t.sync();break}catch(r){if(r.code!=="EEXIST")throw r;try{const s=await u.stat(this.mutationLockPath);if(Date.now()-s.mtimeMs>K){await u.unlink(this.mutationLockPath);continue}}catch{continue}await new Promise(s=>setTimeout(s,5))}if(!t)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");try{return await e()}finally{await t.close().catch(()=>{}),await u.unlink(this.mutationLockPath).catch(()=>{})}}async transitionRun(e,t){$(e);const n=await this.readState(t),r=n.runControl,s=r?.receipts.find(l=>l.operationId===e.operationId||l.idempotencyKey===e.idempotencyKey);if(s){if(s.operationId===e.operationId&&s.idempotencyKey===e.idempotencyKey&&s.payloadHash===e.payloadHash)return s;throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}if(r&&r.runId!==e.runId)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if((r?.revision??0)!==e.expectedRevision)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if((r?.latestSnapshotHash??null)!==e.expectedSnapshotHash)throw new Error("YOLOOP_RUN_SNAPSHOT_CONFLICT");B(r,e);const c=e.next.receipts.at(-1);if(!c)throw new Error("YOLOOP_RUN_OPERATION_CONFLICT");return await this.writeState({...n,version:"1.2",runControl:e.next}),this.logger.info({event:"yoloop_run_transition_committed",runId:e.runId,operationId:e.operationId,revision:c.revision,status:c.status},"yoloop run transition committed"),c}trySetFilePerms(e){try{k(e)}catch{}}trySetDirPerms(e){try{x(e)}catch{}}async prepareLockForSnapshot(e,t){if(!e)return null;if(e.ownerKind!=="client")return e;const n=N(e,t),r={...e,leaseState:n};if(n==="expired")return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"lease_expired"},"expired client-owned yoloop lock lease recovered before snapshot echo"),null;if(!r.pid||r.pid<=0)return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"invalid_pid"},"invalid PID in client-owned yoloop lock; recovered"),null;if(r.hostname&&r.hostname!==f())return{...r,ownerPidState:"unknown"};try{return process.kill(r.pid,0),{...r,ownerPidState:"live"}}catch{return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"dead_pid"},"stale client-owned yoloop lock recovered before snapshot echo"),null}}async maybeRecoverOrphanLock(e){const t=await this.readLock();if(!t)return!0;if(N(t,e)==="expired")return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"lease_expired"},"expired yoloop lock lease recovered"),!0;if(!t.pid||t.pid<=0)return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"invalid_pid"},"invalid PID in lock file; recovered"),!0;if(t.hostname&&t.hostname!==f())return this.logger.warn({event:"yoloop_lock_remote_host",ownerKind:t.ownerKind??"server",ownerPidState:"unknown"},"lock held by a remote host; honoring"),!1;try{return process.kill(t.pid,0),!1}catch{return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"dead_pid"},"orphan yoloop lock recovered"),!0}}async deleteLockBestEffort(){try{await u.unlink(this.lockPath)}catch{}}isSameClientSessionLock(e,t){return e.ownerKind==="client"&&e.sessionId===t.sessionId&&e.pid===process.pid&&e.hostname===f()}async cleanupOrphanTempFiles(){try{const e=C(this.statePath),t=await u.readdir(e),n=`${m}.tmp.`,r=t.filter(s=>s.startsWith(n));for(const s of r)try{await u.unlink(w(e,s))}catch{}}catch{}}}function h(o){return o===null||typeof o!="object"?JSON.stringify(o):Array.isArray(o)?`[${o.map(h).join(",")}]`:`{${Object.entries(o).filter(([,e])=>e!==void 0).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${h(t)}`).join(",")}}`}function y(o,e){return P("sha256").update(`${o}\0${h(e)}`).digest("hex")}function A(o){const{receipts:e,...t}=o.next;return y("p220-yoloop-run-transition-payload-v1",{runId:o.runId,operationId:o.operationId,idempotencyKey:o.idempotencyKey,expectedRevision:o.expectedRevision,expectedSnapshotHash:o.expectedSnapshotHash,next:t})}const g=new Set(["success","partial","blocked","failure","exhausted","cancelled"]),q={created:["running","cancelling","exhausted"],running:["pausing","cancelling","success","partial","blocked","failure","exhausted"],pausing:["paused","cancelling","failure","exhausted"],paused:["resuming","cancelling","exhausted"],resuming:["running","cancelling","failure","exhausted"],cancelling:["cancelled","failure","exhausted"]};function M(o,e){return e==="pause"?o==="running"?"pausing":null:e==="resume"?o==="paused"?"resuming":null:g.has(o)||o==="cancelling"?null:"cancelling"}function V(o){const e=o.current.revision+1,t=o.action==="cancel"?["YOLOOP_RUN_CANCEL_REQUESTED"]:[],n={schemaVersion:"YoloopOperationalBudgetSummary/1.0",limits:o.current.operationalBudget.limits,usage:o.current.usage,exhaustedDimension:null},r={schemaVersion:"ProgressSnapshot/1.0",runId:o.current.runId,sequence:o.current.progressSnapshots.length+1,revision:e,operationId:o.operationId,boundary:o.action,phase:"before",status:o.current.status,occurredAt:o.occurredAt,parentSnapshotHash:o.current.latestSnapshotHash,budget:n,reasonCodes:t,publicSafe:!0},s=y("p220-yoloop-progress-snapshot-id-v1",{runId:r.runId,operationId:r.operationId,sequence:r.sequence,phase:r.phase}),i={...r,snapshotId:s,snapshotHash:y("p220-yoloop-progress-snapshot-v1",{...r,snapshotId:s})},a={...r,sequence:i.sequence+1,phase:"after",status:o.target,parentSnapshotHash:i.snapshotHash},c=y("p220-yoloop-progress-snapshot-id-v1",{runId:a.runId,operationId:a.operationId,sequence:a.sequence,phase:a.phase}),l={...a,snapshotId:c,snapshotHash:y("p220-yoloop-progress-snapshot-v1",{...a,snapshotId:c})},d={schemaVersion:"YoloopRunEvent/1.0",runId:o.current.runId,operationId:o.operationId,kind:"intent",sequence:o.current.events.length+1,revision:e,occurredAt:o.occurredAt,fromStatus:o.current.status,toStatus:o.target,reasonCodes:t,budgetDimension:null,parentEventHash:o.current.events.at(-1)?.eventHash??null,publicSafe:!0},p=y("p220-yoloop-run-event-id-v1",{runId:d.runId,operationId:d.operationId,sequence:d.sequence}),_={...d,eventId:p,eventHash:y("p220-yoloop-run-event-v1",{...d,eventId:p})},O={...o.current,revision:e,status:o.target,events:[...o.current.events,_],progressSnapshots:[...o.current.progressSnapshots,i,l],latestSnapshotHash:l.snapshotHash,receipts:[...o.current.receipts]},v={runId:o.current.runId,operationId:o.operationId,idempotencyKey:o.idempotencyKey,expectedRevision:o.current.revision,expectedSnapshotHash:o.current.latestSnapshotHash,next:O},S=A(v),L={schemaVersion:"YoloopRunTransitionReceipt/1.0",runId:o.current.runId,operationId:o.operationId,idempotencyKey:o.idempotencyKey,payloadHash:S,revision:e,snapshotHash:l.snapshotHash,status:o.target};return{type:"transition_run",...v,payloadHash:S,next:{...O,receipts:[...O.receipts,L]}}}function $(o){const e=/^[A-Za-z0-9._:-]{1,128}$/;if(!e.test(o.runId)||!e.test(o.operationId)||!e.test(o.idempotencyKey)||!/^[a-f0-9]{64}$/.test(o.payloadHash)||!Number.isSafeInteger(o.expectedRevision)||o.expectedRevision<0)throw new Error("YOLOOP_RUN_OPERATION_CONFLICT");if(o.payloadHash!==A(o))throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}function B(o,e){const t=e.next,n=o?.events??[],r=o?.progressSnapshots??[],s=o?.receipts??[];if(t.schemaVersion!=="YoloopRunControl/1.0"||t.runId!==e.runId||t.revision!==e.expectedRevision+1||t.events.length!==n.length+1||t.progressSnapshots.length!==r.length+2||t.receipts.length!==s.length+1||h(t.events.slice(0,n.length))!==h(n)||h(t.progressSnapshots.slice(0,r.length))!==h(r)||h(t.receipts.slice(0,s.length))!==h(s))throw new Error("YOLOOP_RUN_TRANSITION_INVALID");if(o){if(g.has(o.status))throw new Error("YOLOOP_RUN_TERMINAL");const d=t.progressSnapshots.at(-2)?.boundary,p=o.status===t.status&&(d==="budget"||d==="retry");if(!(q[o.status]??[]).includes(t.status)&&!p)throw new Error("YOLOOP_RUN_TRANSITION_INVALID");if(o.startedAt!==t.startedAt||o.deadlineAt!==t.deadlineAt)throw new Error("YOLOOP_RUN_TRANSITION_INVALID")}else if(t.status!=="created")throw new Error("YOLOOP_RUN_TRANSITION_INVALID");const i=t.progressSnapshots.at(-2),a=t.progressSnapshots.at(-1),c=t.events.at(-1),l=t.receipts.at(-1);if(!i||!a||!c||!l||i.phase!=="before"||a.phase!=="after"||i.operationId!==e.operationId||a.operationId!==e.operationId||c.operationId!==e.operationId||i.runId!==e.runId||a.runId!==e.runId||c.runId!==e.runId||i.revision!==t.revision||a.revision!==t.revision||c.revision!==t.revision||i.parentSnapshotHash!==(o?.latestSnapshotHash??null)||a.parentSnapshotHash!==i.snapshotHash||t.latestSnapshotHash!==a.snapshotHash||l.schemaVersion!=="YoloopRunTransitionReceipt/1.0"||l.runId!==e.runId||l.operationId!==e.operationId||l.idempotencyKey!==e.idempotencyKey||l.payloadHash!==e.payloadHash||l.revision!==t.revision||l.snapshotHash!==t.latestSnapshotHash||l.status!==t.status||Date.parse(t.deadlineAt)<=Date.parse(t.startedAt))throw new Error("YOLOOP_RUN_TRANSITION_INVALID");E(i),E(a),J(c,n.at(-1)?.eventHash??null)}function E(o){const{snapshotHash:e,...t}=o;if(e!==y("p220-yoloop-progress-snapshot-v1",t))throw new Error("YOLOOP_RUN_SNAPSHOT_CONFLICT")}function J(o,e){const{eventHash:t,...n}=o;if(o.parentEventHash!==e||t!==y("p220-yoloop-run-event-v1",n))throw new Error("YOLOOP_RUN_SNAPSHOT_CONFLICT")}function j(o,e){return`lease-${W(`${o}:${e}`)}`}function W(o){let e=2166136261;for(let t=0;t<o.length;t+=1)e^=o.charCodeAt(t),e=Math.imul(e,16777619)>>>0;return e.toString(36).padStart(7,"0")}function N(o,e){const t=o.leaseExpiresAt??o.ttlExpiresAt,n=Date.parse(t),r=Date.parse(e);return!Number.isFinite(n)||!Number.isFinite(r)?"unknown":n<=r?"expired":"active"}function G(o,e){const t=I(e);return{...o,version:o.version==="1.2"?"1.2":"1.1",lastResetAt:o.lastResetAt??t.lastResetAt,storiesRunToday:o.storiesRunToday??0,iterationsToday:o.iterationsToday??0,iterationsTodayDate:o.iterationsTodayDate??t.iterationsTodayDate,tokensUsedToday:o.tokensUsedToday??0,lastRunAt:o.lastRunAt??null,coolDownUntil:o.coolDownUntil??null,totalRunsAllTime:o.totalRunsAllTime??0,totalStoriesAllTime:o.totalStoriesAllTime??0,currentSession:o.currentSession??null,...o.runControl!==void 0?{runControl:o.runControl}:{},...o.continuationPolicy!==void 0?{continuationPolicy:o.continuationPolicy}:{},...o.continuationSession!==void 0?{continuationSession:o.continuationSession}:{},...o.pendingContinuationAuthorities!==void 0?{pendingContinuationAuthorities:o.pendingContinuationAuthorities}:{},...o.continuationReceipts!==void 0?{continuationReceipts:o.continuationReceipts}:{},...o.continuationRecoveryReceipts!==void 0?{continuationRecoveryReceipts:o.continuationRecoveryReceipts}:{},recovery_history:Array.isArray(o.recovery_history)?T(o.recovery_history):[]}}function T(o){return o.length<=R?o:o.slice(o.length-R)}export{ne as YoloopClientStateStore,A as buildYoloopRunTransitionPayloadHash,T as capRecoveryHistory,I as createDefaultCounters,oe as createDefaultState};
|
|
@@ -161,6 +161,12 @@ const POLICY_KEYS = ['billingClass', 'idempotencyClass', 'minimumTier', 'mutatio
|
|
|
161
161
|
const COMPATIBILITY_KEYS = ['deprecatedIn', 'introducedIn', 'removedIn', 'replacement'];
|
|
162
162
|
const DESCRIPTION_KEYS = ['en', 'es', 'pt-BR'];
|
|
163
163
|
const PROJECTIONS = ['admin', 'client', 'docs', 'metrics', 'server', 'shared'];
|
|
164
|
+
export const COMMAND_REGISTRY_PROJECTION_KINDS = Object.freeze([...PROJECTIONS]);
|
|
165
|
+
const PUBLIC_COMMAND_FIELDS = [
|
|
166
|
+
'aliases', 'category', 'compatibility', 'description', 'examples', 'id',
|
|
167
|
+
'inputShape', 'mode', 'name', 'number', 'policy', 'projections', 'stepId',
|
|
168
|
+
'subcommands', 'visibility',
|
|
169
|
+
];
|
|
164
170
|
function exactKeys(value, expected) {
|
|
165
171
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
166
172
|
fail('REGISTRY_SCHEMA_INVALID');
|
|
@@ -380,3 +386,35 @@ export function parseCommandRegistryBytes(bytes) {
|
|
|
380
386
|
export function commandRegistryChecksum(document) {
|
|
381
387
|
return createHash('sha256').update(canonicalizeCommandRegistryJcs(document)).digest('hex');
|
|
382
388
|
}
|
|
389
|
+
/** P220.19: derive every compatibility view from one admitted registry value. */
|
|
390
|
+
export function projectCommandRegistryDocument(document, kind) {
|
|
391
|
+
if (!PROJECTIONS.includes(kind))
|
|
392
|
+
fail('REGISTRY_PROJECTION_DRIFT');
|
|
393
|
+
const admitted = normalizeCommandRegistryDocument(document);
|
|
394
|
+
const commands = admitted.commands.filter((entry) => entry.projections.includes(kind));
|
|
395
|
+
if (kind === 'server') {
|
|
396
|
+
return { schemaVersion: 1, registryVersion: admitted.registryVersion, commands: commands.map(({ invocation: _invocation, ...entry }) => entry) };
|
|
397
|
+
}
|
|
398
|
+
if (kind === 'shared') {
|
|
399
|
+
return {
|
|
400
|
+
schemaVersion: 1,
|
|
401
|
+
registryVersion: admitted.registryVersion,
|
|
402
|
+
commands: commands.map(({ handlerRef: _handlerRef, handlerClass: _handlerClass, invocation: _invocation, subcommands, ...entry }) => ({
|
|
403
|
+
...entry,
|
|
404
|
+
subcommands: subcommands.map(({ handlerRef: _ref, handlerClass: _class, ...operation }) => operation),
|
|
405
|
+
})),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
schemaVersion: 1,
|
|
410
|
+
registryVersion: admitted.registryVersion,
|
|
411
|
+
commands: commands
|
|
412
|
+
.filter((entry) => entry.visibility === 'public')
|
|
413
|
+
.map((entry) => Object.fromEntries(PUBLIC_COMMAND_FIELDS.map((field) => [
|
|
414
|
+
field,
|
|
415
|
+
field === 'subcommands'
|
|
416
|
+
? entry.subcommands.map(({ handlerRef: _ref, handlerClass: _class, ...operation }) => operation)
|
|
417
|
+
: entry[field],
|
|
418
|
+
]))),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license FSL-1.1
|
|
3
|
+
* Copyright (c) 2026 OrNexus AI
|
|
4
|
+
*
|
|
5
|
+
* P220.03 -- Single typed authority for public Goal operation identity.
|
|
6
|
+
*
|
|
7
|
+
* Portal control actions are transport vocabulary. They are intentionally
|
|
8
|
+
* mapped in one direction and are never accepted as implicit CLI aliases.
|
|
9
|
+
*/
|
|
10
|
+
export const CONTINUITY_GOAL_OPERATION_REGISTRY_VERSION = '1.0.0';
|
|
11
|
+
const compatibility = Object.freeze({
|
|
12
|
+
introducedIn: CONTINUITY_GOAL_OPERATION_REGISTRY_VERSION,
|
|
13
|
+
deprecatedIn: null,
|
|
14
|
+
removedIn: null,
|
|
15
|
+
replacement: null,
|
|
16
|
+
});
|
|
17
|
+
export const CONTINUITY_GOAL_OPERATION_DEFINITIONS = Object.freeze([
|
|
18
|
+
{ id: 'answer-decision', aliases: Object.freeze([]), compatibility },
|
|
19
|
+
{ id: 'approve-review-gate', aliases: Object.freeze([]), compatibility },
|
|
20
|
+
{ id: 'cancel', aliases: Object.freeze([]), compatibility },
|
|
21
|
+
{ id: 'deny-review-gate', aliases: Object.freeze([]), compatibility },
|
|
22
|
+
{ id: 'pause', aliases: Object.freeze([]), compatibility },
|
|
23
|
+
{ id: 'plan', aliases: Object.freeze([]), compatibility },
|
|
24
|
+
{ id: 'report', aliases: Object.freeze([]), compatibility },
|
|
25
|
+
{ id: 'resume', aliases: Object.freeze([]), compatibility },
|
|
26
|
+
{ id: 'start', aliases: Object.freeze([]), compatibility },
|
|
27
|
+
{ id: 'status', aliases: Object.freeze([]), compatibility },
|
|
28
|
+
{ id: 'tick', aliases: Object.freeze([]), compatibility },
|
|
29
|
+
]);
|
|
30
|
+
export const CONTINUITY_GOAL_OPERATION_IDS = Object.freeze(CONTINUITY_GOAL_OPERATION_DEFINITIONS.map((definition) => definition.id));
|
|
31
|
+
const GOAL_OPERATION_BY_ID = new Map(CONTINUITY_GOAL_OPERATION_DEFINITIONS.map((definition) => [definition.id, definition]));
|
|
32
|
+
const GOAL_OPERATION_BY_ALIAS = new Map(CONTINUITY_GOAL_OPERATION_DEFINITIONS.flatMap((definition) => definition.aliases.map((alias) => [alias, definition])));
|
|
33
|
+
export function isContinuityGoalOperation(value) {
|
|
34
|
+
return GOAL_OPERATION_BY_ID.has(value);
|
|
35
|
+
}
|
|
36
|
+
export function resolveContinuityGoalOperation(value) {
|
|
37
|
+
return GOAL_OPERATION_BY_ID.get(value) ?? GOAL_OPERATION_BY_ALIAS.get(value) ?? null;
|
|
38
|
+
}
|
|
39
|
+
export const CONTINUITY_GOAL_GATE_OPERATIONS = Object.freeze([
|
|
40
|
+
'answer-decision',
|
|
41
|
+
'approve-review-gate',
|
|
42
|
+
'deny-review-gate',
|
|
43
|
+
]);
|
|
44
|
+
export const CONTINUITY_GOAL_GATE_TO_PORTAL_ACTION = Object.freeze({
|
|
45
|
+
'answer-decision': 'answer_decision',
|
|
46
|
+
'approve-review-gate': 'approve_review_gate',
|
|
47
|
+
'deny-review-gate': 'deny_review_gate',
|
|
48
|
+
});
|