shennian 0.5.24 → 0.5.25

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.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "jsMinified": true,
4
4
  "minifier": "esbuild",
5
- "fileCount": 281,
5
+ "fileCount": 282,
6
6
  "files": [
7
7
  {
8
8
  "file": "bin/shennian-adapter-host.js",
@@ -881,8 +881,8 @@
881
881
  },
882
882
  {
883
883
  "file": "src/room/host-session-observations.js",
884
- "beforeBytes": 9518,
885
- "afterBytes": 5220
884
+ "beforeBytes": 9514,
885
+ "afterBytes": 5216
886
886
  },
887
887
  {
888
888
  "file": "src/room/invite-bootstrap.js",
@@ -964,10 +964,15 @@
964
964
  "beforeBytes": 3278,
965
965
  "afterBytes": 2276
966
966
  },
967
+ {
968
+ "file": "src/room/runtime-state-path.js",
969
+ "beforeBytes": 1608,
970
+ "afterBytes": 808
971
+ },
967
972
  {
968
973
  "file": "src/room/session-registry.js",
969
- "beforeBytes": 30643,
970
- "afterBytes": 17214
974
+ "beforeBytes": 30639,
975
+ "afterBytes": 17210
971
976
  },
972
977
  {
973
978
  "file": "src/room/wait-for-messages.js",
@@ -1,2 +1,2 @@
1
- import p from"node:crypto";import r from"node:fs";import{renameSyncWithRetry as P}from"../fs/atomic-rename.js";import u from"node:path";import{isRoomRuntimeAdapter as H}from"@shennian/wire";import{resolveShennianPath as I}from"../config/index.js";const h=/^[a-f0-9]{64}$/,g=/^[A-Za-z][A-Za-z0-9]{0,63}$/;class n extends Error{code;constructor(t,o){super(o),this.code=t,this.name="RoomHostSessionObservationError"}}class F{filePath;state;constructor(t=I("runtime","room-host-session-observations.json")){this.filePath=t,this.state=this.load()}observe(t){S(t);const o=f(t),s=this.state.records[o],a=(t.now??new Date).toISOString(),l=!!(s&&t.eventName==="SessionStart"&&(s.lifecycleState==="closed"||s.hostContextHash!==t.hostContextHash)),c=s?.lifecycleState==="closed"&&t.eventName!=="SessionStart"?"closed":t.lifecycleState,k=l?null:s?.lastActiveAt??null,_=l?null:s?.lastIdleAt??null,d={schemaVersion:1,observationKey:o,machineId:t.machineId,runtimeAdapter:t.runtimeAdapter,sourceSessionKey:t.sourceSessionKey,hostContextHash:t.hostContextHash,workspaceHash:t.workspaceHash??s?.workspaceHash??null,workspacePath:t.workspacePath??s?.workspacePath??null,lifecycleState:c,registrationEpoch:s?s.registrationEpoch+(l?1:0):1,lastEventName:t.eventName,observedAt:s?.observedAt??a,lastSeenAt:a,lastActiveAt:c==="active"?a:k,lastIdleAt:c==="idle"?a:_,closedAt:c==="closed"?s?.closedAt??a:null};return this.state.records[o]=d,this.persist(),m(d)}getBySource(t){const o=this.state.records[f(t)];return o?m(o):null}list(){return Object.values(this.state.records).sort((t,o)=>t.observedAt.localeCompare(o.observedAt)).map(m)}load(){try{const t=r.lstatSync(this.filePath);if(!t.isFile()||t.isSymbolicLink())throw b();return process.platform!=="win32"&&(t.mode&63)!==0&&r.chmodSync(this.filePath,384),x(JSON.parse(r.readFileSync(this.filePath,"utf8")))}catch(t){if(t.code==="ENOENT")return{schemaVersion:1,records:{}};throw t instanceof n?t:new n("corrupt_observation_store","Room host Session observations are corrupt.")}}persist(){const t=u.dirname(this.filePath);r.mkdirSync(t,{recursive:!0,mode:448}),process.platform!=="win32"&&r.chmodSync(t,448);const o=`${this.filePath}.tmp-${process.pid}-${p.randomBytes(6).toString("hex")}`;try{if(r.writeFileSync(o,`${JSON.stringify(this.state,null,2)}
2
- `,{encoding:"utf8",mode:384,flag:"wx"}),process.platform!=="win32"&&r.chmodSync(o,384),process.platform==="win32"&&r.existsSync(this.filePath)){const s=r.lstatSync(this.filePath);if(!s.isFile()||s.isSymbolicLink())throw b();r.unlinkSync(this.filePath)}P(o,this.filePath),process.platform!=="win32"&&r.chmodSync(this.filePath,384)}finally{try{r.unlinkSync(o)}catch{}}}}function f(e){return v(e.machineId),y(e.runtimeAdapter),A(e.sourceSessionKey),p.createHash("sha256").update(`${e.machineId}\0${e.runtimeAdapter}\0${e.sourceSessionKey}`,"utf8").digest("hex")}function S(e){v(e.machineId),y(e.runtimeAdapter),A(e.sourceSessionKey),h.test(e.hostContextHash)||i("host_context_hash"),e.workspaceHash!=null&&!h.test(e.workspaceHash)&&i("workspace_hash"),e.workspacePath!=null&&w(e.workspacePath),["active","idle","closed"].includes(e.lifecycleState)||i("lifecycle_state"),g.test(e.eventName)||i("event_name")}function v(e){/^[A-Za-z0-9_.:@/-]{1,512}$/.test(e)||i("machine_id")}function y(e){H(e)||i("runtime_adapter")}function A(e){(!e||e.length>1024||/[\0\r\n]/.test(e))&&i("source_session_key")}function x(e){if(!e||typeof e!="object"||e.schemaVersion!==1)throw new n("corrupt_observation_store","Unsupported observation store.");const t=e.records;if(!t||typeof t!="object"||Array.isArray(t))throw new n("corrupt_observation_store","Invalid observation records.");const o={};for(const[s,a]of Object.entries(t)){if(N(a),s!==a.observationKey||s!==f(a))throw new n("corrupt_observation_store","Observation key mismatch.");o[s]={...a,workspacePath:a.workspacePath??null,lastActiveAt:a.lastActiveAt??(a.lifecycleState==="active"?a.lastSeenAt:null),lastIdleAt:a.lastIdleAt??(a.lifecycleState==="idle"?a.lastSeenAt:null)}}return{schemaVersion:1,records:o}}function N(e){(!e||e.schemaVersion!==1)&&i("observation"),S({machineId:e.machineId,runtimeAdapter:e.runtimeAdapter,sourceSessionKey:e.sourceSessionKey,hostContextHash:e.hostContextHash,workspaceHash:e.workspaceHash,workspacePath:e.workspacePath??null,lifecycleState:e.lifecycleState,eventName:e.lastEventName}),h.test(e.observationKey)||i("observation_key"),(!Number.isInteger(e.registrationEpoch)||e.registrationEpoch<1)&&i("registration_epoch");for(const t of[e.observedAt,e.lastSeenAt])Number.isFinite(Date.parse(t))||i("observation_time");for(const t of[e.lastActiveAt,e.lastIdleAt])t!=null&&!Number.isFinite(Date.parse(t))&&i("transition_time");e.closedAt!=null&&!Number.isFinite(Date.parse(e.closedAt))&&i("closed_at"),e.workspacePath!=null&&w(e.workspacePath)}function w(e){(!u.isAbsolute(e)||e.length>4096||/[\0\r\n]/.test(e))&&i("workspace_path")}function i(e){throw new n(`invalid_${e}`,`Invalid ${e}.`)}function b(){return new n("unsafe_observation_store","Room host Session observation store is not a regular file.")}function m(e){return structuredClone(e)}export{n as RoomHostSessionObservationError,F as RoomHostSessionObservationStore,f as buildRoomHostSessionObservationKey};
1
+ import p from"node:crypto";import r from"node:fs";import{renameSyncWithRetry as P}from"../fs/atomic-rename.js";import S from"node:path";import{isRoomRuntimeAdapter as H}from"@shennian/wire";import{resolveRoomStateFile as I}from"./runtime-state-path.js";const h=/^[a-f0-9]{64}$/,g=/^[A-Za-z][A-Za-z0-9]{0,63}$/;class n extends Error{code;constructor(t,o){super(o),this.code=t,this.name="RoomHostSessionObservationError"}}class F{filePath;state;constructor(t=I("room-host-session-observations.json")){this.filePath=t,this.state=this.load()}observe(t){u(t);const o=f(t),s=this.state.records[o],a=(t.now??new Date).toISOString(),l=!!(s&&t.eventName==="SessionStart"&&(s.lifecycleState==="closed"||s.hostContextHash!==t.hostContextHash)),c=s?.lifecycleState==="closed"&&t.eventName!=="SessionStart"?"closed":t.lifecycleState,k=l?null:s?.lastActiveAt??null,_=l?null:s?.lastIdleAt??null,d={schemaVersion:1,observationKey:o,machineId:t.machineId,runtimeAdapter:t.runtimeAdapter,sourceSessionKey:t.sourceSessionKey,hostContextHash:t.hostContextHash,workspaceHash:t.workspaceHash??s?.workspaceHash??null,workspacePath:t.workspacePath??s?.workspacePath??null,lifecycleState:c,registrationEpoch:s?s.registrationEpoch+(l?1:0):1,lastEventName:t.eventName,observedAt:s?.observedAt??a,lastSeenAt:a,lastActiveAt:c==="active"?a:k,lastIdleAt:c==="idle"?a:_,closedAt:c==="closed"?s?.closedAt??a:null};return this.state.records[o]=d,this.persist(),m(d)}getBySource(t){const o=this.state.records[f(t)];return o?m(o):null}list(){return Object.values(this.state.records).sort((t,o)=>t.observedAt.localeCompare(o.observedAt)).map(m)}load(){try{const t=r.lstatSync(this.filePath);if(!t.isFile()||t.isSymbolicLink())throw b();return process.platform!=="win32"&&(t.mode&63)!==0&&r.chmodSync(this.filePath,384),x(JSON.parse(r.readFileSync(this.filePath,"utf8")))}catch(t){if(t.code==="ENOENT")return{schemaVersion:1,records:{}};throw t instanceof n?t:new n("corrupt_observation_store","Room host Session observations are corrupt.")}}persist(){const t=S.dirname(this.filePath);r.mkdirSync(t,{recursive:!0,mode:448}),process.platform!=="win32"&&r.chmodSync(t,448);const o=`${this.filePath}.tmp-${process.pid}-${p.randomBytes(6).toString("hex")}`;try{if(r.writeFileSync(o,`${JSON.stringify(this.state,null,2)}
2
+ `,{encoding:"utf8",mode:384,flag:"wx"}),process.platform!=="win32"&&r.chmodSync(o,384),process.platform==="win32"&&r.existsSync(this.filePath)){const s=r.lstatSync(this.filePath);if(!s.isFile()||s.isSymbolicLink())throw b();r.unlinkSync(this.filePath)}P(o,this.filePath),process.platform!=="win32"&&r.chmodSync(this.filePath,384)}finally{try{r.unlinkSync(o)}catch{}}}}function f(e){return v(e.machineId),y(e.runtimeAdapter),A(e.sourceSessionKey),p.createHash("sha256").update(`${e.machineId}\0${e.runtimeAdapter}\0${e.sourceSessionKey}`,"utf8").digest("hex")}function u(e){v(e.machineId),y(e.runtimeAdapter),A(e.sourceSessionKey),h.test(e.hostContextHash)||i("host_context_hash"),e.workspaceHash!=null&&!h.test(e.workspaceHash)&&i("workspace_hash"),e.workspacePath!=null&&w(e.workspacePath),["active","idle","closed"].includes(e.lifecycleState)||i("lifecycle_state"),g.test(e.eventName)||i("event_name")}function v(e){/^[A-Za-z0-9_.:@/-]{1,512}$/.test(e)||i("machine_id")}function y(e){H(e)||i("runtime_adapter")}function A(e){(!e||e.length>1024||/[\0\r\n]/.test(e))&&i("source_session_key")}function x(e){if(!e||typeof e!="object"||e.schemaVersion!==1)throw new n("corrupt_observation_store","Unsupported observation store.");const t=e.records;if(!t||typeof t!="object"||Array.isArray(t))throw new n("corrupt_observation_store","Invalid observation records.");const o={};for(const[s,a]of Object.entries(t)){if(N(a),s!==a.observationKey||s!==f(a))throw new n("corrupt_observation_store","Observation key mismatch.");o[s]={...a,workspacePath:a.workspacePath??null,lastActiveAt:a.lastActiveAt??(a.lifecycleState==="active"?a.lastSeenAt:null),lastIdleAt:a.lastIdleAt??(a.lifecycleState==="idle"?a.lastSeenAt:null)}}return{schemaVersion:1,records:o}}function N(e){(!e||e.schemaVersion!==1)&&i("observation"),u({machineId:e.machineId,runtimeAdapter:e.runtimeAdapter,sourceSessionKey:e.sourceSessionKey,hostContextHash:e.hostContextHash,workspaceHash:e.workspaceHash,workspacePath:e.workspacePath??null,lifecycleState:e.lifecycleState,eventName:e.lastEventName}),h.test(e.observationKey)||i("observation_key"),(!Number.isInteger(e.registrationEpoch)||e.registrationEpoch<1)&&i("registration_epoch");for(const t of[e.observedAt,e.lastSeenAt])Number.isFinite(Date.parse(t))||i("observation_time");for(const t of[e.lastActiveAt,e.lastIdleAt])t!=null&&!Number.isFinite(Date.parse(t))&&i("transition_time");e.closedAt!=null&&!Number.isFinite(Date.parse(e.closedAt))&&i("closed_at"),e.workspacePath!=null&&w(e.workspacePath)}function w(e){(!S.isAbsolute(e)||e.length>4096||/[\0\r\n]/.test(e))&&i("workspace_path")}function i(e){throw new n(`invalid_${e}`,`Invalid ${e}.`)}function b(){return new n("unsafe_observation_store","Room host Session observation store is not a regular file.")}function m(e){return structuredClone(e)}export{n as RoomHostSessionObservationError,F as RoomHostSessionObservationStore,f as buildRoomHostSessionObservationKey};
@@ -0,0 +1 @@
1
+ import o from"node:fs";import n from"node:path";import{getShennianDir as f}from"../config/index.js";function u(c,{stateDir:s=f(),supervisor:a=process.env.SHENNIAN_DAEMON_SUPERVISOR}={}){const i=n.join(s,"runtime",c);if(a!=="docker")return i;const t=n.join(s,"room-state");o.mkdirSync(t,{recursive:!0,mode:448});const m=o.lstatSync(t);if(!m.isDirectory()||m.isSymbolicLink())throw new Error("unsafe_room_state_directory");process.platform!=="win32"&&o.chmodSync(t,448);const e=n.join(t,c);if(!o.existsSync(e))try{const r=o.lstatSync(i);if(!r.isFile()||r.isSymbolicLink())throw new Error("unsafe_legacy_room_state");o.copyFileSync(i,e,o.constants.COPYFILE_EXCL),process.platform!=="win32"&&o.chmodSync(e,384)}catch(r){if(r.code!=="ENOENT"&&r.code!=="EEXIST")throw r}return e}export{u as resolveRoomStateFile};
@@ -1,2 +1,2 @@
1
- import I from"node:crypto";import u from"node:fs";import{renameSyncWithRetry as P}from"../fs/atomic-rename.js";import M from"node:path";import{isRoomRuntimeAdapter as C}from"@shennian/wire";import{resolveShennianPath as D}from"../config/index.js";const f=500,m=20,T=/^[A-Za-z0-9_.:@/-]{1,512}$/,b=new Set(["pending_approval","active","paused","detached"]),S=new Set(["auto_follow","mention_only","paused"]);class o extends Error{code;constructor(e,i){super(i),this.code=e,this.name="RoomSessionRegistryError"}}class V{filePath;state;mutexTails=new Map;constructor(e=D("runtime","room-session-registry.json"),i=new Date){this.filePath=e,this.state=this.load(),this.markPersistedActivationsInterrupted(i)&&this.persist()}register(e){c(e.machineId,"machine_id"),_(e.runtimeAdapter),A(e.sourceSessionKey),c(e.nianSessionId,"nian_session_id"),c(e.hostProcessIdentity,"host_process_identity"),q(e.capabilities);const i=p(e),s=(e.now??new Date).toISOString(),n=this.state.records[i];if(n?.lifecycleState==="closed")throw new o("session_registration_closed","A retired session cannot be registered again.");if(Object.values(this.state.records).find(g=>g.registryKey!==i&&g.nianSessionId===e.nianSessionId&&g.lifecycleState!=="closed"))throw new o("nian_session_already_registered","This Shennian session is already registered to another native Agent session.");const a=!!(n&&n.hostProcessIdentity!==e.hostProcessIdentity),l={schemaVersion:1,registryKey:i,machineId:e.machineId,runtimeAdapter:e.runtimeAdapter,sourceSessionKey:e.sourceSessionKey,nianSessionId:e.nianSessionId,panelContextKey:e.nianSessionId,hostProcessIdentity:e.hostProcessIdentity,registrationEpoch:n?n.registrationEpoch+(a?1:0):1,lifecycleState:"active",capabilities:{...e.capabilities},binding:n?.binding??null,registeredAt:n?.registeredAt??s,lastSeenAt:s,updatedAt:s};return this.state.records[i]=l,this.persist(),d(l)}getBySource(e){const i=this.state.records[p(e)];return i?d(i):null}getByNianSessionId(e){c(e,"nian_session_id");const i=Object.values(this.state.records).find(s=>s.nianSessionId===e&&s.lifecycleState!=="closed");return i?d(i):null}getByRegistryKey(e){if(!/^[a-f0-9]{64}$/.test(e))throw new o("invalid_registry_key","Invalid Room session registry key.");const i=this.state.records[e];return i?d(i):null}list(){return Object.values(this.state.records).sort((e,i)=>e.registeredAt.localeCompare(i.registeredAt)).map(d)}setBinding(e){const i=this.requireRecord(e.registryKey);for(const[a,l]of Object.entries({binding_id:e.bindingId,room_id:e.roomId,agent_identity_id:e.agentIdentityId}))c(l,a);if(!b.has(e.status))throw new o("invalid_binding_status","Invalid binding status.");if(e.status==="pending_approval"&&e.agentMembershipId!==null||(e.status==="active"||e.status==="paused")&&e.agentMembershipId===null)throw new o("invalid_agent_membership_id","Pending Room bindings cannot claim an Agent membership.");e.agentMembershipId!==null&&c(e.agentMembershipId,"agent_membership_id");const s=e.activationMode??"auto_follow";if(!S.has(s))throw new o("invalid_activation_mode","Invalid activation mode.");if(i.binding&&i.binding.status!=="detached"&&(i.binding.bindingId!==e.bindingId||i.binding.roomId!==e.roomId))throw new o("session_already_bound","An Agent session can have only one active Room binding.");if(i.lifecycleState==="closed")throw new o("session_registration_closed","A retired session cannot be rebound.");e.initialSequence!==void 0&&h(e.initialSequence);const n=Object.values(this.state.records).filter(a=>a.registryKey!==i.registryKey&&a.lifecycleState!=="closed"&&a.binding?.bindingId===e.bindingId&&a.binding.status!=="detached");if(e.status!=="detached"&&n.length&&!e.replaceOwner)throw new o("binding_already_owned","A Room binding can have only one live session.");if(e.replaceOwner)for(const a of Object.values(this.state.records))a.registryKey===i.registryKey||a.binding?.bindingId!==e.bindingId||(a.lifecycleState="closed",a.registrationEpoch+=1,a.binding.status="detached",a.binding.activationMode="paused",a.binding.activationQueue=[],a.binding.activeActivation=null,a.binding.mentionContext=[],a.updatedAt=(e.now??new Date).toISOString());const r=i.binding?.activationMode;return i.binding={bindingId:e.bindingId,roomId:e.roomId,agentIdentityId:e.agentIdentityId,agentMembershipId:e.agentMembershipId,status:e.status,activationMode:s,lastDeliveredSequence:i.binding?.lastDeliveredSequence??e.initialSequence??"0",lastActivatedSequence:i.binding?.lastActivatedSequence??e.initialSequence??"0",activationQueue:i.binding?.activationQueue??[],activeActivation:i.binding?.activeActivation??null,mentionContext:r==="mention_only"&&s==="mention_only"?i.binding?.mentionContext??[]:[],loopBreaker:i.binding?.loopBreaker??{windowStartedAt:null,activationCount:0,pausedUntil:null}},this.touchAndPersist(i,e.now),d(i)}detachBinding(e,i=new Date){const s=this.requireRecord(e);return s.binding&&(s.binding.status="detached",s.binding.activationMode="paused",s.binding.activationQueue=[],s.binding.activeActivation=null,s.binding.mentionContext=[]),this.touchAndPersist(s,i),d(s)}advanceCursor(e){const i=this.requireBoundRecord(e.registryKey);h(e.sequence);const s=e.cursor==="delivered"?"lastDeliveredSequence":"lastActivatedSequence";if(BigInt(e.sequence)<BigInt(i.binding[s]))throw new o("cursor_regression","Room cursor cannot move backwards.");return i.binding[s]=e.sequence,this.touchAndPersist(i,e.now),d(i)}enqueueActivation(e,i,s=new Date,n=!1){const r=this.requireBoundRecord(e);y(i);const a=r.binding.activationQueue;if(!a.some(l=>l.causeMessageId===i.causeMessageId)){if(a.length>=f)throw new o("activation_queue_full","Agent activation queue is full.");a.push(d(i))}return n&&(r.binding.mentionContext=[]),this.touchAndPersist(r,s),d(r)}appendMentionContext(e,i,s=new Date){const n=this.requireBoundRecord(e);w(i);const r=n.binding.mentionContext??[];return r.some(a=>a.sequence===i.sequence)||(r.push(d(i)),n.binding.mentionContext=r.slice(-m)),this.touchAndPersist(n,s),d(n)}replaceMentionContext(e,i,s=new Date){const n=this.requireBoundRecord(e);for(const r of i)w(r);return n.binding.mentionContext=i.slice(-m).map(d),this.touchAndPersist(n,s),d(n)}retireLegacyActivationState(e,i=new Date){const s=this.requireBoundRecord(e);return s.binding.activationQueue=[],s.binding.activeActivation=null,this.touchAndPersist(s,i),d(s)}dequeueActivation(e,i,s=new Date){const n=this.requireBoundRecord(e);return n.binding.activationQueue=n.binding.activationQueue.filter(r=>r.causeMessageId!==i),this.touchAndPersist(n,s),d(n)}markActivationDispatching(e,i,s=new Date){const n=this.requireBoundRecord(e),r=n.binding.activationQueue.find(a=>a.causeMessageId===i);if(!r)throw new o("activation_not_found","Agent activation is unavailable.");return n.binding.activeActivation={...d(r),dispatchState:"dispatching",dispatchedAt:s.toISOString(),interruptedAt:void 0},n.binding.activationQueue=n.binding.activationQueue.filter(a=>a.causeMessageId!==i),this.touchAndPersist(n,s),d(n)}clearActiveActivation(e,i=new Date){const s=this.requireBoundRecord(e);return s.binding.activeActivation=null,this.touchAndPersist(s,i),d(s)}retryActiveActivation(e){const i=this.requireBoundRecord(e.registryKey),s=i.binding.activeActivation;if(!s)return{queued:!1,attempts:0};if(x(s))throw new o("activation_interrupted_requires_explicit_retry","Interrupted Agent activation requires an explicit retry.");return this.requeueActiveActivation(i,e)}requeueActiveActivation(e,i){const s=e.binding.activeActivation,n=i.now??new Date,{dispatchState:r,dispatchedAt:a,interruptedAt:l,...g}=s,v={...g,attempts:s.attempts+1};return e.binding.activeActivation=null,v.attempts<i.maxAttempts?(e.binding.activationQueue.some(R=>R.causeMessageId===v.causeMessageId)||e.binding.activationQueue.unshift(v),this.touchAndPersist(e,n),{queued:!0,attempts:v.attempts}):(e.binding.loopBreaker.pausedUntil=new Date(n.getTime()+i.pauseMs).toISOString(),this.touchAndPersist(e,n),{queued:!1,attempts:v.attempts})}markPersistedActivationsInterrupted(e){const i=e.toISOString();let s=!1;for(const n of Object.values(this.state.records)){const r=n.binding?.activeActivation;!r||x(r)||(r.dispatchState="interrupted",r.interruptedAt=i,n.updatedAt=i,s=!0)}return s}claimActivation(e){const i=this.requireBoundRecord(e.registryKey),s=e.now??new Date,n=i.binding.loopBreaker;return e.causeMessageId&&n.claimedMessageIds?.includes(e.causeMessageId)?{allowed:!0,pausedUntil:null}:n.pausedUntil&&Date.parse(n.pausedUntil)>s.getTime()?{allowed:!1,pausedUntil:n.pausedUntil,newlyPaused:!1}:((!n.windowStartedAt||s.getTime()-Date.parse(n.windowStartedAt)>=e.windowMs)&&(n.windowStartedAt=s.toISOString(),n.activationCount=0,n.pausedUntil=null),n.activationCount>=e.maxActivations?(n.pausedUntil=new Date(s.getTime()+e.pauseMs).toISOString(),this.touchAndPersist(i,s),{allowed:!1,pausedUntil:n.pausedUntil,newlyPaused:!0}):(n.activationCount+=1,e.causeMessageId&&(n.claimedMessageIds=[...n.claimedMessageIds??[],e.causeMessageId].slice(-f)),this.touchAndPersist(i,s),{allowed:!0,pausedUntil:null}))}updateLifecycle(e,i,s=new Date){const n=this.requireRecord(e);if(n.lifecycleState==="closed"&&i!=="closed")throw new o("session_registration_closed","A retired session cannot be reopened.");return n.lifecycleState!==i&&(n.registrationEpoch+=1),n.lifecycleState=i,i==="closed"&&n.binding&&(n.binding.status="detached",n.binding.activationMode="paused",n.binding.activationQueue=[],n.binding.activeActivation=null,n.binding.mentionContext=[]),this.touchAndPersist(n,s),d(n)}async runExclusive(e,i){this.requireRecord(e);const s=this.mutexTails.get(e)??Promise.resolve();let n;const r=new Promise(l=>{n=l}),a=s.then(()=>r);this.mutexTails.set(e,a),await s;try{return await i()}finally{n(),this.mutexTails.get(e)===a&&this.mutexTails.delete(e)}}requireRecord(e){const i=this.state.records[e];if(!i)throw new o("session_registration_not_found","Agent session is not registered.");return i}requireBoundRecord(e){const i=this.requireRecord(e);if(!i.binding||i.binding.status!=="active"&&i.binding.status!=="paused")throw new o("session_not_bound","Agent session has no active Room binding.");return i}touchAndPersist(e,i=new Date){e.updatedAt=i.toISOString(),this.persist()}load(){try{const e=u.lstatSync(this.filePath);if(!e.isFile()||e.isSymbolicLink())throw new o("unsafe_session_registry","Room session registry is not a regular file.");process.platform!=="win32"&&(e.mode&63)!==0&&u.chmodSync(this.filePath,384);const i=JSON.parse(u.readFileSync(this.filePath,"utf8"));return O(i)}catch(e){if(Q(e))return{schemaVersion:1,records:{}};throw e instanceof o?e:new o("corrupt_session_registry","Room Agent session registry is corrupt.")}}persist(){const e=M.dirname(this.filePath);u.mkdirSync(e,{recursive:!0,mode:448}),process.platform!=="win32"&&u.chmodSync(e,448);const i=`${this.filePath}.tmp-${process.pid}-${I.randomBytes(6).toString("hex")}`;try{if(u.writeFileSync(i,`${JSON.stringify(this.state,null,2)}
1
+ import I from"node:crypto";import u from"node:fs";import{renameSyncWithRetry as P}from"../fs/atomic-rename.js";import M from"node:path";import{isRoomRuntimeAdapter as C}from"@shennian/wire";import{resolveRoomStateFile as D}from"./runtime-state-path.js";const f=500,m=20,T=/^[A-Za-z0-9_.:@/-]{1,512}$/,b=new Set(["pending_approval","active","paused","detached"]),S=new Set(["auto_follow","mention_only","paused"]);class o extends Error{code;constructor(e,i){super(i),this.code=e,this.name="RoomSessionRegistryError"}}class V{filePath;state;mutexTails=new Map;constructor(e=D("room-session-registry.json"),i=new Date){this.filePath=e,this.state=this.load(),this.markPersistedActivationsInterrupted(i)&&this.persist()}register(e){c(e.machineId,"machine_id"),_(e.runtimeAdapter),A(e.sourceSessionKey),c(e.nianSessionId,"nian_session_id"),c(e.hostProcessIdentity,"host_process_identity"),q(e.capabilities);const i=p(e),s=(e.now??new Date).toISOString(),n=this.state.records[i];if(n?.lifecycleState==="closed")throw new o("session_registration_closed","A retired session cannot be registered again.");if(Object.values(this.state.records).find(g=>g.registryKey!==i&&g.nianSessionId===e.nianSessionId&&g.lifecycleState!=="closed"))throw new o("nian_session_already_registered","This Shennian session is already registered to another native Agent session.");const a=!!(n&&n.hostProcessIdentity!==e.hostProcessIdentity),l={schemaVersion:1,registryKey:i,machineId:e.machineId,runtimeAdapter:e.runtimeAdapter,sourceSessionKey:e.sourceSessionKey,nianSessionId:e.nianSessionId,panelContextKey:e.nianSessionId,hostProcessIdentity:e.hostProcessIdentity,registrationEpoch:n?n.registrationEpoch+(a?1:0):1,lifecycleState:"active",capabilities:{...e.capabilities},binding:n?.binding??null,registeredAt:n?.registeredAt??s,lastSeenAt:s,updatedAt:s};return this.state.records[i]=l,this.persist(),d(l)}getBySource(e){const i=this.state.records[p(e)];return i?d(i):null}getByNianSessionId(e){c(e,"nian_session_id");const i=Object.values(this.state.records).find(s=>s.nianSessionId===e&&s.lifecycleState!=="closed");return i?d(i):null}getByRegistryKey(e){if(!/^[a-f0-9]{64}$/.test(e))throw new o("invalid_registry_key","Invalid Room session registry key.");const i=this.state.records[e];return i?d(i):null}list(){return Object.values(this.state.records).sort((e,i)=>e.registeredAt.localeCompare(i.registeredAt)).map(d)}setBinding(e){const i=this.requireRecord(e.registryKey);for(const[a,l]of Object.entries({binding_id:e.bindingId,room_id:e.roomId,agent_identity_id:e.agentIdentityId}))c(l,a);if(!b.has(e.status))throw new o("invalid_binding_status","Invalid binding status.");if(e.status==="pending_approval"&&e.agentMembershipId!==null||(e.status==="active"||e.status==="paused")&&e.agentMembershipId===null)throw new o("invalid_agent_membership_id","Pending Room bindings cannot claim an Agent membership.");e.agentMembershipId!==null&&c(e.agentMembershipId,"agent_membership_id");const s=e.activationMode??"auto_follow";if(!S.has(s))throw new o("invalid_activation_mode","Invalid activation mode.");if(i.binding&&i.binding.status!=="detached"&&(i.binding.bindingId!==e.bindingId||i.binding.roomId!==e.roomId))throw new o("session_already_bound","An Agent session can have only one active Room binding.");if(i.lifecycleState==="closed")throw new o("session_registration_closed","A retired session cannot be rebound.");e.initialSequence!==void 0&&h(e.initialSequence);const n=Object.values(this.state.records).filter(a=>a.registryKey!==i.registryKey&&a.lifecycleState!=="closed"&&a.binding?.bindingId===e.bindingId&&a.binding.status!=="detached");if(e.status!=="detached"&&n.length&&!e.replaceOwner)throw new o("binding_already_owned","A Room binding can have only one live session.");if(e.replaceOwner)for(const a of Object.values(this.state.records))a.registryKey===i.registryKey||a.binding?.bindingId!==e.bindingId||(a.lifecycleState="closed",a.registrationEpoch+=1,a.binding.status="detached",a.binding.activationMode="paused",a.binding.activationQueue=[],a.binding.activeActivation=null,a.binding.mentionContext=[],a.updatedAt=(e.now??new Date).toISOString());const r=i.binding?.activationMode;return i.binding={bindingId:e.bindingId,roomId:e.roomId,agentIdentityId:e.agentIdentityId,agentMembershipId:e.agentMembershipId,status:e.status,activationMode:s,lastDeliveredSequence:i.binding?.lastDeliveredSequence??e.initialSequence??"0",lastActivatedSequence:i.binding?.lastActivatedSequence??e.initialSequence??"0",activationQueue:i.binding?.activationQueue??[],activeActivation:i.binding?.activeActivation??null,mentionContext:r==="mention_only"&&s==="mention_only"?i.binding?.mentionContext??[]:[],loopBreaker:i.binding?.loopBreaker??{windowStartedAt:null,activationCount:0,pausedUntil:null}},this.touchAndPersist(i,e.now),d(i)}detachBinding(e,i=new Date){const s=this.requireRecord(e);return s.binding&&(s.binding.status="detached",s.binding.activationMode="paused",s.binding.activationQueue=[],s.binding.activeActivation=null,s.binding.mentionContext=[]),this.touchAndPersist(s,i),d(s)}advanceCursor(e){const i=this.requireBoundRecord(e.registryKey);h(e.sequence);const s=e.cursor==="delivered"?"lastDeliveredSequence":"lastActivatedSequence";if(BigInt(e.sequence)<BigInt(i.binding[s]))throw new o("cursor_regression","Room cursor cannot move backwards.");return i.binding[s]=e.sequence,this.touchAndPersist(i,e.now),d(i)}enqueueActivation(e,i,s=new Date,n=!1){const r=this.requireBoundRecord(e);y(i);const a=r.binding.activationQueue;if(!a.some(l=>l.causeMessageId===i.causeMessageId)){if(a.length>=f)throw new o("activation_queue_full","Agent activation queue is full.");a.push(d(i))}return n&&(r.binding.mentionContext=[]),this.touchAndPersist(r,s),d(r)}appendMentionContext(e,i,s=new Date){const n=this.requireBoundRecord(e);w(i);const r=n.binding.mentionContext??[];return r.some(a=>a.sequence===i.sequence)||(r.push(d(i)),n.binding.mentionContext=r.slice(-m)),this.touchAndPersist(n,s),d(n)}replaceMentionContext(e,i,s=new Date){const n=this.requireBoundRecord(e);for(const r of i)w(r);return n.binding.mentionContext=i.slice(-m).map(d),this.touchAndPersist(n,s),d(n)}retireLegacyActivationState(e,i=new Date){const s=this.requireBoundRecord(e);return s.binding.activationQueue=[],s.binding.activeActivation=null,this.touchAndPersist(s,i),d(s)}dequeueActivation(e,i,s=new Date){const n=this.requireBoundRecord(e);return n.binding.activationQueue=n.binding.activationQueue.filter(r=>r.causeMessageId!==i),this.touchAndPersist(n,s),d(n)}markActivationDispatching(e,i,s=new Date){const n=this.requireBoundRecord(e),r=n.binding.activationQueue.find(a=>a.causeMessageId===i);if(!r)throw new o("activation_not_found","Agent activation is unavailable.");return n.binding.activeActivation={...d(r),dispatchState:"dispatching",dispatchedAt:s.toISOString(),interruptedAt:void 0},n.binding.activationQueue=n.binding.activationQueue.filter(a=>a.causeMessageId!==i),this.touchAndPersist(n,s),d(n)}clearActiveActivation(e,i=new Date){const s=this.requireBoundRecord(e);return s.binding.activeActivation=null,this.touchAndPersist(s,i),d(s)}retryActiveActivation(e){const i=this.requireBoundRecord(e.registryKey),s=i.binding.activeActivation;if(!s)return{queued:!1,attempts:0};if(x(s))throw new o("activation_interrupted_requires_explicit_retry","Interrupted Agent activation requires an explicit retry.");return this.requeueActiveActivation(i,e)}requeueActiveActivation(e,i){const s=e.binding.activeActivation,n=i.now??new Date,{dispatchState:r,dispatchedAt:a,interruptedAt:l,...g}=s,v={...g,attempts:s.attempts+1};return e.binding.activeActivation=null,v.attempts<i.maxAttempts?(e.binding.activationQueue.some(R=>R.causeMessageId===v.causeMessageId)||e.binding.activationQueue.unshift(v),this.touchAndPersist(e,n),{queued:!0,attempts:v.attempts}):(e.binding.loopBreaker.pausedUntil=new Date(n.getTime()+i.pauseMs).toISOString(),this.touchAndPersist(e,n),{queued:!1,attempts:v.attempts})}markPersistedActivationsInterrupted(e){const i=e.toISOString();let s=!1;for(const n of Object.values(this.state.records)){const r=n.binding?.activeActivation;!r||x(r)||(r.dispatchState="interrupted",r.interruptedAt=i,n.updatedAt=i,s=!0)}return s}claimActivation(e){const i=this.requireBoundRecord(e.registryKey),s=e.now??new Date,n=i.binding.loopBreaker;return e.causeMessageId&&n.claimedMessageIds?.includes(e.causeMessageId)?{allowed:!0,pausedUntil:null}:n.pausedUntil&&Date.parse(n.pausedUntil)>s.getTime()?{allowed:!1,pausedUntil:n.pausedUntil,newlyPaused:!1}:((!n.windowStartedAt||s.getTime()-Date.parse(n.windowStartedAt)>=e.windowMs)&&(n.windowStartedAt=s.toISOString(),n.activationCount=0,n.pausedUntil=null),n.activationCount>=e.maxActivations?(n.pausedUntil=new Date(s.getTime()+e.pauseMs).toISOString(),this.touchAndPersist(i,s),{allowed:!1,pausedUntil:n.pausedUntil,newlyPaused:!0}):(n.activationCount+=1,e.causeMessageId&&(n.claimedMessageIds=[...n.claimedMessageIds??[],e.causeMessageId].slice(-f)),this.touchAndPersist(i,s),{allowed:!0,pausedUntil:null}))}updateLifecycle(e,i,s=new Date){const n=this.requireRecord(e);if(n.lifecycleState==="closed"&&i!=="closed")throw new o("session_registration_closed","A retired session cannot be reopened.");return n.lifecycleState!==i&&(n.registrationEpoch+=1),n.lifecycleState=i,i==="closed"&&n.binding&&(n.binding.status="detached",n.binding.activationMode="paused",n.binding.activationQueue=[],n.binding.activeActivation=null,n.binding.mentionContext=[]),this.touchAndPersist(n,s),d(n)}async runExclusive(e,i){this.requireRecord(e);const s=this.mutexTails.get(e)??Promise.resolve();let n;const r=new Promise(l=>{n=l}),a=s.then(()=>r);this.mutexTails.set(e,a),await s;try{return await i()}finally{n(),this.mutexTails.get(e)===a&&this.mutexTails.delete(e)}}requireRecord(e){const i=this.state.records[e];if(!i)throw new o("session_registration_not_found","Agent session is not registered.");return i}requireBoundRecord(e){const i=this.requireRecord(e);if(!i.binding||i.binding.status!=="active"&&i.binding.status!=="paused")throw new o("session_not_bound","Agent session has no active Room binding.");return i}touchAndPersist(e,i=new Date){e.updatedAt=i.toISOString(),this.persist()}load(){try{const e=u.lstatSync(this.filePath);if(!e.isFile()||e.isSymbolicLink())throw new o("unsafe_session_registry","Room session registry is not a regular file.");process.platform!=="win32"&&(e.mode&63)!==0&&u.chmodSync(this.filePath,384);const i=JSON.parse(u.readFileSync(this.filePath,"utf8"));return O(i)}catch(e){if(Q(e))return{schemaVersion:1,records:{}};throw e instanceof o?e:new o("corrupt_session_registry","Room Agent session registry is corrupt.")}}persist(){const e=M.dirname(this.filePath);u.mkdirSync(e,{recursive:!0,mode:448}),process.platform!=="win32"&&u.chmodSync(e,448);const i=`${this.filePath}.tmp-${process.pid}-${I.randomBytes(6).toString("hex")}`;try{if(u.writeFileSync(i,`${JSON.stringify(this.state,null,2)}
2
2
  `,{encoding:"utf8",mode:384,flag:"wx"}),process.platform!=="win32"&&u.chmodSync(i,384),process.platform==="win32"&&u.existsSync(this.filePath)){const s=u.lstatSync(this.filePath);if(!s.isFile()||s.isSymbolicLink())throw new o("unsafe_session_registry","Refusing to replace an unsafe Room session registry.");u.unlinkSync(this.filePath)}P(i,this.filePath),process.platform!=="win32"&&u.chmodSync(this.filePath,384)}finally{try{u.unlinkSync(i)}catch{}}}}function p(t){return c(t.machineId,"machine_id"),_(t.runtimeAdapter),A(t.sourceSessionKey),I.createHash("sha256").update(`${t.machineId}\0${t.runtimeAdapter}\0${t.sourceSessionKey}`,"utf8").digest("hex")}function O(t){if(!t||typeof t!="object"||t.schemaVersion!==1)throw new o("corrupt_session_registry","Unsupported Room session registry.");const e=t.records;if(!e||typeof e!="object"||Array.isArray(e))throw new o("corrupt_session_registry","Room session records are invalid.");const i={};for(const[s,n]of Object.entries(e)){if(B(n),s!==n.registryKey||s!==p(n))throw new o("corrupt_session_registry","Room session registry key mismatch.");i[s]={...n,binding:n.binding?{...n.binding,activeActivation:n.binding.activeActivation??null,mentionContext:n.binding.mentionContext??[]}:null}}return{schemaVersion:1,records:i}}function B(t){if(!t||t.schemaVersion!==1)throw new o("corrupt_session_registry","Invalid registration.");if(c(t.machineId,"machine_id"),_(t.runtimeAdapter),A(t.sourceSessionKey),c(t.nianSessionId,"nian_session_id"),c(t.hostProcessIdentity,"host_process_identity"),t.panelContextKey!==t.nianSessionId)throw new o("corrupt_session_registry","Invalid panel context.");if(!Number.isInteger(t.registrationEpoch)||t.registrationEpoch<1)throw new o("corrupt_session_registry","Invalid registration epoch.");q(t.capabilities),t.binding&&E(t.binding)}function E(t){if(c(t.bindingId,"binding_id"),c(t.roomId,"room_id"),c(t.agentIdentityId,"agent_identity_id"),!b.has(t.status))throw new o("corrupt_session_registry","Invalid binding status.");if(t.status==="pending_approval"&&t.agentMembershipId!==null||(t.status==="active"||t.status==="paused")&&t.agentMembershipId===null)throw new o("corrupt_session_registry","Invalid Agent membership.");if(t.agentMembershipId!==null&&c(t.agentMembershipId,"agent_membership_id"),!S.has(t.activationMode))throw new o("corrupt_session_registry","Invalid activation mode.");if(h(t.lastDeliveredSequence),h(t.lastActivatedSequence),!Array.isArray(t.activationQueue)||t.activationQueue.length>f)throw new o("corrupt_session_registry","Invalid activation queue.");if(t.activationQueue.forEach(y),t.activeActivation!=null&&y(t.activeActivation),t.mentionContext!==void 0&&(!Array.isArray(t.mentionContext)||t.mentionContext.length>m))throw new o("corrupt_session_registry","Invalid mention context.");t.mentionContext?.forEach(w)}function w(t){if(h(t.sequence),typeof t.deliveryText!="string"||!t.deliveryText.trim()||t.deliveryText.length>24e3)throw new o("invalid_mention_context","Invalid mention context.")}function y(t){if(c(t.causeMessageId,"cause_message_id"),c(t.rootCauseMessageId,"root_cause_message_id"),h(t.sequence),!Number.isInteger(t.hopCount)||t.hopCount<0||t.hopCount>32)throw new o("invalid_hop_count","Invalid Agent activation hop count.");if(!Number.isInteger(t.attempts)||t.attempts<0||t.attempts>100)throw new o("invalid_activation_attempts","Invalid Agent activation attempts.");if(!Number.isFinite(new Date(t.enqueuedAt).getTime()))throw new o("invalid_activation_time","Invalid Agent activation time.");if(t.deliveryText!==void 0&&(typeof t.deliveryText!="string"||!t.deliveryText.trim()||t.deliveryText.length>24e3))throw new o("invalid_activation_text","Invalid Agent activation text.");if(t.systemPrompt!==void 0&&(typeof t.systemPrompt!="string"||!t.systemPrompt.trim()||t.systemPrompt.length>24e3))throw new o("invalid_activation_prompt","Invalid Agent activation prompt.");if(t.promptSnapshot!==void 0&&!k(t.promptSnapshot))throw new o("invalid_activation_prompt_snapshot","Invalid Agent activation prompt snapshot.");if(t.dispatchState!==void 0&&t.dispatchState!=="dispatching"&&t.dispatchState!=="interrupted")throw new o("invalid_activation_state","Invalid Agent activation state.");if(t.dispatchedAt!==void 0&&!Number.isFinite(new Date(t.dispatchedAt).getTime()))throw new o("invalid_activation_dispatch_time","Invalid Agent activation dispatch time.");if(t.dispatchState===void 0&&t.dispatchedAt!==void 0)throw new o("invalid_activation_dispatch_time","Queued Agent activation cannot have a dispatch time.");if(t.dispatchState==="interrupted"&&(t.interruptedAt===void 0||!Number.isFinite(new Date(t.interruptedAt).getTime())))throw new o("invalid_activation_interrupted_time","Invalid Agent activation interruption time.");if(t.dispatchState!=="interrupted"&&t.interruptedAt!==void 0)throw new o("invalid_activation_interrupted_time","Unexpected Agent activation interruption time.")}function k(t){if(!t||typeof t!="object"||Array.isArray(t))return!1;const e=t,i=s=>s===null||typeof s=="string"&&/^[a-f0-9]{64}$/.test(s);return typeof e.managedRoomPolicyVersion=="string"&&e.managedRoomPolicyVersion.length<=64&&i(e.sessionModeInstructionsHash)&&i(e.agentSystemPromptHash)&&typeof e.trustedRoomContextHash=="string"&&/^[a-f0-9]{64}$/.test(e.trustedRoomContextHash)}function x(t){return t.dispatchState==="interrupted"}function q(t){if(!t||["canIdentifySession","canBindAutomatically","canOpenEmbeddedRoomPage","canResumeIdleSession","canActivateWhileHostOpen"].some(i=>typeof t[i]!="boolean"))throw new o("invalid_session_capabilities","Invalid Agent session capabilities.")}function _(t){if(!C(t))throw new o("invalid_runtime_adapter","Unsupported Room runtime adapter.")}function A(t){if(!t||t.length>1024||/[\0\r\n]/.test(t))throw new o("invalid_source_session_key","Invalid native Agent session key.")}function c(t,e){if(!T.test(t))throw new o(`invalid_${e}`,`Invalid ${e}.`)}function h(t){if(!/^(0|[1-9]\d{0,19})$/.test(t))throw new o("invalid_room_sequence","Invalid Room sequence.")}function d(t){return structuredClone(t)}function Q(t){return typeof t=="object"&&t!==null&&"code"in t&&t.code==="ENOENT"}export{V as RoomSessionRegistry,o as RoomSessionRegistryError,p as buildRoomSessionRegistryKey,x as isInterruptedRoomActivation};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.5.24",
3
+ "version": "0.5.25",
4
4
  "description": "Shennian \u2014 AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {