shennian 0.5.23 → 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",
@@ -571,8 +571,8 @@
571
571
  },
572
572
  {
573
573
  "file": "src/commands/upgrade.js",
574
- "beforeBytes": 14049,
575
- "afterBytes": 7260
574
+ "beforeBytes": 14344,
575
+ "afterBytes": 7396
576
576
  },
577
577
  {
578
578
  "file": "src/commands/worker.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",
@@ -1386,8 +1391,8 @@
1386
1391
  },
1387
1392
  {
1388
1393
  "file": "src/upgrade/updater-launcher.js",
1389
- "beforeBytes": 10380,
1390
- "afterBytes": 5299
1394
+ "beforeBytes": 10435,
1395
+ "afterBytes": 5350
1391
1396
  },
1392
1397
  {
1393
1398
  "file": "src/upgrade/updater-protocol.js",
@@ -1 +1 @@
1
- import c from"chalk";import{execSync as V,spawn as P}from"node:child_process";import{randomUUID as v}from"node:crypto";import _ from"node:os";import{createInterface as $}from"node:readline/promises";import{getCurrentVersion as y,fetchLatestVersion as k,compareVersions as w,checkForUpdate as A,acquireUpgradeLock as M,recordUpgradeFailure as F}from"../upgrade/engine.js";import{callDaemonIpc as D}from"../daemon-ipc/client.js";import{getDaemonStatus as O}from"./daemon.js";import{ensureUpdaterInstalled as U}from"../upgrade/updater-bootstrap.js";import{launchUpdater as S,createUpdaterJob as j,waitForUpdaterReady as H,confirmUpdaterHandoff as q}from"../upgrade/updater-launcher.js";import{prepareRecoveryServiceEntry as C}from"../upgrade/updater-service-entry.js";import{resolveShennianPath as L}from"../config/index.js";import{assertUpdaterAvailable as N,prepareUpdaterCandidate as R}from"../upgrade/updater-worker.js";import{getUpgradeChannel as T,setUpgradeChannel as G}from"../upgrade/channel.js";import{fetchOwnedCliArtifact as b,getCliUpdateSourceOrder as x}from"../upgrade/distribution.js";const I=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function Y(n,r){return n!=="major"||r}function ie(n={}){const r=n.platform??_.platform(),i=n.kill??process.kill,e=n.pid??process.pid,p=n.env??process.env,t=n.spawn??P,s=n.execSync??V;if(r==="win32"){const o=p.ComSpec||"cmd.exe";try{t(o,["/d","/s","/c","timeout /t 2 /nobreak >nul & schtasks /Run /TN ShennianAgent >nul"],{detached:!0,stdio:"ignore",windowsHide:!0}).unref(),i(e,"SIGTERM");return}catch{try{s('schtasks /run /tn "ShennianAgent"',{stdio:"pipe",windowsHide:!0})}catch{}}}else try{t("sh",["-c","sleep 2; shennian start >/dev/null 2>&1"],{detached:!0,stdio:"ignore",env:p}).unref()}catch{}i(e,"SIGTERM")}function ce(n){n.command("upgrade").description("Upgrade shennian CLI to the latest version").option("--channel <channel>","Persist stable (npm latest) or preview (npm next); does not install").option("--check","Only check for updates, do not install").option("--to <version>","Upgrade to a specific version").option("--yes","Confirm a major version upgrade (required in non-interactive environments)").action(async r=>{if(r.channel!==void 0){if(r.to||r.yes)throw new Error("--channel cannot be combined with --to or --yes");if(G(r.channel),console.log(`Upgrade channel: ${T()} (saved; no installation)`),!r.check)return}console.log(`Upgrade channel: ${T()}`);const i=y();console.log(c.gray(`Current version: ${i}`)),console.log();let e=0;const p=setInterval(()=>{process.stdout.write(`\r ${I[e%I.length]} Checking for updates...`),e++},80);let t;try{t=r.to??await k(),clearInterval(p),process.stdout.write("\r"+" ".repeat(40)+"\r")}catch(o){clearInterval(p),process.stdout.write("\r"+" ".repeat(40)+"\r"),console.error(c.red(`\u2717 Failed to check for updates: ${o instanceof Error?o.message:String(o)}`)),process.exit(1)}const s=w(i,t);if(s==="none"&&!r.to){console.log(c.green(`\u2713 Already up to date (${i})`));return}if(console.log(` Available: ${c.green(t)} ${s==="major"?c.red("(major)"):s==="minor"?c.yellow("(minor)"):c.green("(patch)")}`),console.log(),!r.check){if(s==="major"&&(console.log(c.yellow(" \u26A0 This is a major version update and may contain breaking changes.")),console.log(c.gray(" Please review the changelog before upgrading.")),console.log(),!r.yes)){(!process.stdin.isTTY||!process.stdout.isTTY)&&(console.error(c.red("\u2717 Major upgrades require explicit confirmation; rerun with --yes.")),process.exit(1));const o=$({input:process.stdin,output:process.stdout}),d=await o.question(` Type ${t} to confirm this major upgrade: `);if(o.close(),d.trim()!==t){console.log(c.gray(" Upgrade cancelled."));return}}try{await D({method:"upgrade.start",params:{targetVersion:t,confirmedMajor:s==="major"||r.yes===!0},timeoutMs:1e4})}catch(o){const d=O({cleanupStale:!0});if(d.running){const a=o&&typeof o=="object"&&"code"in o?o.code:null;throw process.platform==="win32"&&a==="ipc_access_denied"&&d.daemonVersion&&w(d.daemonVersion,"0.5.12")!=="none"?new Error("legacy_s4u_migration_required: The older Windows daemon is running in an inaccessible S4U session. The upgrade was not scheduled; inspect shennian status --json and request assisted migration. Do not force-stop the daemon.",{cause:o}):new Error("A Shennian daemon process is running, but its local control IPC did not accept the upgrade. Run shennian status --json and inspect ipc.state, the Client version, service account and logs. The upgrade was not scheduled; do not force-stop the daemon.",{cause:o})}const m=U(),u={fromVersion:i,targetVersion:t,oldDaemonPid:null,jobId:v()},g=j(u);await R(g,{fetchOwnedArtifact:b,sourceOrder:x()}),C(g,m.workerPath),S(u,{readiness:m})}console.log(),console.log(c.green(`\u2713 Upgrade scheduled ${i} \u2192 ${t}`)),console.log(c.gray(" The independent updater will verify, roll back if needed, and restart Shennian."))}})}async function de(n,r,i,e={}){const p=a=>{n.sendEvent({type:"event",event:"upgrade",payload:a})},t=e.currentVersion??y();let s;try{p({state:"checking",machineId:"self"}),s=i??await k()}catch(a){const l=`Version check failed: ${a instanceof Error?a.message:String(a)}`;return e.suppressResponse||n.sendRes({type:"res",id:r,ok:!1,error:l}),{ok:!1,error:l}}const o=w(t,s);if(o==="none")return e.suppressResponse||n.sendRes({type:"res",id:r,ok:!0,payload:{alreadyLatest:!0,version:t}}),{ok:!0,alreadyLatest:!0,version:t};if(!Y(o,e.confirmedMajor===!0))return e.suppressResponse||n.sendRes({type:"res",id:r,ok:!1,error:"Major upgrades require explicit second confirmation"}),{ok:!1,error:"Major upgrades require explicit second confirmation"};const d=v();let m;try{m=M();try{N(L("runtime","updater"))}catch(a){throw m.release(),a}}catch(a){const l=a instanceof Error?a.message:String(a);return e.suppressResponse||n.sendRes({type:"res",id:r,ok:!1,error:l}),{ok:!1,error:l}}const u={from:t,to:s,jobId:d},g=async()=>{let a=!1;try{const l=U(),f={fromVersion:t,targetVersion:s,oldDaemonPid:process.pid,jobId:d,requireHandoffAck:!0},h=j(f);if(p({state:"downloading",machineId:"self",version:s,jobId:d}),await R(h,{fetchOwnedArtifact:b,sourceOrder:x()}),C(h,l.workerPath),S(f,{readiness:l}),await H(h),e.canHandoff&&!e.canHandoff())throw new Error("upgrade_waiting_for_idle");const E=e.prepareForUpgrade?.(d)??{activeRunCount:0};return a=!0,q(h),p({state:"restarting",machineId:"self",...u,activeRunCount:E.activeRunCount}),{ok:!0,...u,resultPath:h.resultPath}}catch(l){a&&e.cancelUpgradePreparation?.();const f=l instanceof Error?l.message:String(l);return f!=="upgrade_waiting_for_idle"&&F(s,f),p({state:"failed",machineId:"self",error:f,jobId:d}),{ok:!1,error:f}}finally{m.release()}};return e.suppressResponse||n.sendRes({type:"res",id:r,ok:!0,payload:u}),e.waitForCompletion?g():(g(),{ok:!0,...u})}async function le(n,r,i={}){const e=await A(i.currentVersion);n.sendRes({type:"res",id:r,ok:!0,payload:e.hasUpdate?{hasUpdate:!0,current:e.current,latest:e.latest,changeType:e.changeType}:{hasUpdate:!1,current:i.currentVersion??y()}})}export{de as handleUpgradeStart,le as handleUpgradeStatus,Y as hasMajorUpgradeConfirmation,ce as registerUpgradeCommand,ie as restartCurrentDaemonAfterUpgrade};
1
+ import c from"chalk";import{execSync as P,spawn as _}from"node:child_process";import{randomUUID as v}from"node:crypto";import $ from"node:os";import{createInterface as A}from"node:readline/promises";import{getCurrentVersion as y,fetchLatestVersion as k,compareVersions as w,checkForUpdate as M,acquireUpgradeLock as F,recordUpgradeFailure as D}from"../upgrade/engine.js";import{callDaemonIpc as O}from"../daemon-ipc/client.js";import{getDaemonStatus as H}from"./daemon.js";import{ensureUpdaterInstalled as U}from"../upgrade/updater-bootstrap.js";import{launchUpdater as S,createUpdaterJob as C,waitForUpdaterReady as q,confirmUpdaterHandoff as L}from"../upgrade/updater-launcher.js";import{prepareRecoveryServiceEntry as j}from"../upgrade/updater-service-entry.js";import{resolveShennianPath as N}from"../config/index.js";import{assertUpdaterAvailable as G,prepareUpdaterCandidate as R}from"../upgrade/updater-worker.js";import{getUpgradeChannel as T,setUpgradeChannel as Y}from"../upgrade/channel.js";import{fetchOwnedCliArtifact as b,getCliUpdateSourceOrder as x}from"../upgrade/distribution.js";const I=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function z(r,e){return r!=="major"||e}function V(r,e){const a=w(r,e);return a==="none"&&r!==e?w(e,r):a}function ce(r={}){const e=r.platform??$.platform(),a=r.kill??process.kill,n=r.pid??process.pid,p=r.env??process.env,o=r.spawn??_,i=r.execSync??P;if(e==="win32"){const t=p.ComSpec||"cmd.exe";try{o(t,["/d","/s","/c","timeout /t 2 /nobreak >nul & schtasks /Run /TN ShennianAgent >nul"],{detached:!0,stdio:"ignore",windowsHide:!0}).unref(),a(n,"SIGTERM");return}catch{try{i('schtasks /run /tn "ShennianAgent"',{stdio:"pipe",windowsHide:!0})}catch{}}}else try{o("sh",["-c","sleep 2; shennian start >/dev/null 2>&1"],{detached:!0,stdio:"ignore",env:p}).unref()}catch{}a(n,"SIGTERM")}function de(r){r.command("upgrade").description("Upgrade Shennian CLI, or select an older version with --to").option("--channel <channel>","Persist stable (npm latest) or preview (npm next); does not install").option("--check","Only check for updates, do not install").option("--to <version>","Upgrade to a specific version").option("--yes","Confirm a major version change (required in non-interactive environments)").action(async e=>{if(e.channel!==void 0){if(e.to||e.yes)throw new Error("--channel cannot be combined with --to or --yes");if(Y(e.channel),console.log(`Upgrade channel: ${T()} (saved; no installation)`),!e.check)return}console.log(`Upgrade channel: ${T()}`);const a=y();console.log(c.gray(`Current version: ${a}`)),console.log();let n=0;const p=setInterval(()=>{process.stdout.write(`\r ${I[n%I.length]} Checking for updates...`),n++},80);let o;try{o=e.to??await k(),clearInterval(p),process.stdout.write("\r"+" ".repeat(40)+"\r")}catch(t){clearInterval(p),process.stdout.write("\r"+" ".repeat(40)+"\r"),console.error(c.red(`\u2717 Failed to check for updates: ${t instanceof Error?t.message:String(t)}`)),process.exit(1)}const i=V(a,o);if(i==="none"&&!e.to){console.log(c.green(`\u2713 Already up to date (${a})`));return}if(console.log(` Available: ${c.green(o)} ${i==="major"?c.red("(major)"):i==="minor"?c.yellow("(minor)"):c.green("(patch)")}`),console.log(),!e.check){if(i==="major"&&(console.log(c.yellow(" \u26A0 This is a major version change and may contain breaking changes.")),console.log(c.gray(" Please review the changelog before upgrading.")),console.log(),!e.yes)){(!process.stdin.isTTY||!process.stdout.isTTY)&&(console.error(c.red("\u2717 Major version changes require explicit confirmation; rerun with --yes.")),process.exit(1));const t=A({input:process.stdin,output:process.stdout}),d=await t.question(` Type ${o} to confirm this major upgrade: `);if(t.close(),d.trim()!==o){console.log(c.gray(" Upgrade cancelled."));return}}try{await O({method:"upgrade.start",params:{targetVersion:o,confirmedMajor:i==="major"||e.yes===!0},timeoutMs:1e4})}catch(t){const d=H({cleanupStale:!0});if(d.running){const s=t&&typeof t=="object"&&"code"in t?t.code:null;throw process.platform==="win32"&&s==="ipc_access_denied"&&d.daemonVersion&&w(d.daemonVersion,"0.5.12")!=="none"?new Error("legacy_s4u_migration_required: The older Windows daemon is running in an inaccessible S4U session. The upgrade was not scheduled; inspect shennian status --json and request assisted migration. Do not force-stop the daemon.",{cause:t}):new Error("A Shennian daemon process is running, but its local control IPC did not accept the upgrade. Run shennian status --json and inspect ipc.state, the Client version, service account and logs. The upgrade was not scheduled; do not force-stop the daemon.",{cause:t})}const m=U(),u={fromVersion:a,targetVersion:o,oldDaemonPid:null,jobId:v()},h=C(u);await R(h,{fetchOwnedArtifact:b,sourceOrder:x()}),j(h,m.workerPath),S(u,{readiness:m})}console.log(),console.log(c.green(`\u2713 Upgrade scheduled ${a} \u2192 ${o}`)),console.log(c.gray(" The independent updater will verify, roll back if needed, and restart Shennian."))}})}async function le(r,e,a,n={}){const p=s=>{r.sendEvent({type:"event",event:"upgrade",payload:s})},o=n.currentVersion??y();let i;try{p({state:"checking",machineId:"self"}),i=a??await k()}catch(s){const l=`Version check failed: ${s instanceof Error?s.message:String(s)}`;return n.suppressResponse||r.sendRes({type:"res",id:e,ok:!1,error:l}),{ok:!1,error:l}}const t=V(o,i);if(i===o)return n.suppressResponse||r.sendRes({type:"res",id:e,ok:!0,payload:{alreadyLatest:!0,version:o}}),{ok:!0,alreadyLatest:!0,version:o};if(!z(t,n.confirmedMajor===!0))return n.suppressResponse||r.sendRes({type:"res",id:e,ok:!1,error:"Major version changes require explicit second confirmation"}),{ok:!1,error:"Major version changes require explicit second confirmation"};const d=v();let m;try{m=F();try{G(N("runtime","updater"))}catch(s){throw m.release(),s}}catch(s){const l=s instanceof Error?s.message:String(s);return n.suppressResponse||r.sendRes({type:"res",id:e,ok:!1,error:l}),{ok:!1,error:l}}const u={from:o,to:i,jobId:d},h=async()=>{let s=!1;try{const l=U(),f={fromVersion:o,targetVersion:i,oldDaemonPid:process.pid,jobId:d,requireHandoffAck:!0},g=C(f);if(p({state:"downloading",machineId:"self",version:i,jobId:d}),await R(g,{fetchOwnedArtifact:b,sourceOrder:x()}),j(g,l.workerPath),S(f,{readiness:l}),await q(g),n.canHandoff&&!n.canHandoff())throw new Error("upgrade_waiting_for_idle");const E=n.prepareForUpgrade?.(d)??{activeRunCount:0};return s=!0,L(g),p({state:"restarting",machineId:"self",...u,activeRunCount:E.activeRunCount}),{ok:!0,...u,resultPath:g.resultPath}}catch(l){s&&n.cancelUpgradePreparation?.();const f=l instanceof Error?l.message:String(l);return f!=="upgrade_waiting_for_idle"&&D(i,f),p({state:"failed",machineId:"self",error:f,jobId:d}),{ok:!1,error:f}}finally{m.release()}};return n.suppressResponse||r.sendRes({type:"res",id:e,ok:!0,payload:u}),n.waitForCompletion?h():(h(),{ok:!0,...u})}async function pe(r,e,a={}){const n=await M(a.currentVersion);r.sendRes({type:"res",id:e,ok:!0,payload:n.hasUpdate?{hasUpdate:!0,current:n.current,latest:n.latest,changeType:n.changeType}:{hasUpdate:!1,current:a.currentVersion??y()}})}export{V as classifyBidirectionalVersionChange,le as handleUpgradeStart,pe as handleUpgradeStatus,z as hasMajorUpgradeConfirmation,de as registerUpgradeCommand,ce as restartCurrentDaemonAfterUpgrade};
@@ -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};
@@ -1,3 +1,3 @@
1
- import r from"node:fs";import d from"node:path";import{randomUUID as y}from"node:crypto";import{spawn as I,spawnSync as j}from"node:child_process";import{resolveShennianPath as f}from"../config/index.js";import{findPackageJson as P,resolveNpmGlobalPrefixFromPackageJson as _}from"./engine.js";import{ensureUpdaterInstalled as E}from"./updater-bootstrap.js";import{resolveNpmCommand as J}from"./npm-command.js";import{UPDATER_PROTOCOL_VERSION as S}from"./updater-protocol.js";function w(e,o){r.mkdirSync(d.dirname(e),{recursive:!0,mode:448});const a=`${e}.tmp-${process.pid}-${y()}`;try{r.writeFileSync(a,`${JSON.stringify(o,null,2)}
2
- `,{mode:384,flag:"wx"});const n=r.openSync(a,process.platform==="win32"?"r+":"r");try{r.fsyncSync(n)}finally{r.closeSync(n)}if(r.renameSync(a,e),process.platform!=="win32"){const t=r.openSync(d.dirname(e),"r");try{r.fsyncSync(t)}finally{r.closeSync(t)}}}finally{try{r.unlinkSync(a)}catch{}}}function $(e){const o=e.root??f("runtime","updater"),a=P(),n=e.npmPrefix??(a?_(a):null);if(!n)throw new Error("Cannot determine the active npm prefix");const t=e.jobId??y();return{protocolVersion:S,jobId:t,fromVersion:e.fromVersion,targetVersion:e.targetVersion,npmPrefix:n,npmExecutable:e.npmExecutable??J(e.nodeExecutable??process.execPath),nodeExecutable:e.nodeExecutable??process.execPath,packageName:"shennian",operation:e.operation??"upgrade",oldDaemonPid:e.oldDaemonPid,platform:process.platform,createdAt:(e.now??new Date).toISOString(),daemonExitTimeoutMs:15e3,commandTimeoutMs:12e4,resultPath:d.join(o,"results",`${t}.json`),attemptPath:f("upgrade-attempt.json"),...e.requireHandoffAck?{requireHandoffAck:!0}:{},...e.rollbackJobId?{rollbackJobId:e.rollbackJobId}:{},...e.isolatedDaemonStart?{isolatedDaemonStart:!0}:{}}}function O(e,o=f("runtime","updater")){const a=d.join(o,"jobs");r.mkdirSync(a,{recursive:!0,mode:448});const n=d.join(a,`${e.jobId}.json`),t=`${n}.tmp-${process.pid}-${y()}`;try{r.writeFileSync(t,`${JSON.stringify(e,null,2)}
3
- `,{mode:384,flag:"wx"});const c=r.openSync(t,process.platform==="win32"?"r+":"r");try{r.fsyncSync(c)}finally{r.closeSync(c)}if(r.renameSync(t,n),process.platform!=="win32"){const s=r.openSync(a,"r");try{r.fsyncSync(s)}finally{r.closeSync(s)}}return n}finally{try{r.unlinkSync(t)}catch{}}}function v(e,o){return e==="linux"&&/(?:^|\/)shennian\.service(?:\n|$)/m.test(o.trim())}function R(e,o={}){const a=o.root??e.root??f("runtime","updater"),n=o.readiness??E({root:a}),t=$({...e,root:a}),c=O(t,a),s=d.join(a,"updater.lock");r.mkdirSync(a,{recursive:!0,mode:448});let l=null,p=!1;const h=()=>{if(p){try{JSON.parse(r.readFileSync(s,"utf8")).jobId===t.jobId&&r.unlinkSync(s)}catch{}p=!1}};try{try{l=r.openSync(s,"wx",384),p=!0}catch(i){if(i.code!=="EEXIST")throw i;let b=!1;try{const g=JSON.parse(r.readFileSync(s,"utf8")),k=o.isAlive??(x=>{try{return process.kill(x,0),!0}catch{return!1}});b=!!(g.pid&&k(g.pid))}catch{}if(b)throw new Error("upgrade_locked");r.rmSync(s,{force:!0}),l=r.openSync(s,"wx",384),p=!0}r.writeFileSync(l,JSON.stringify({jobId:t.jobId,phase:"handoff",pid:process.pid,createdAt:(o.now??Date.now)()})),r.closeSync(l),l=null;let u=o.serviceCgroup??"";if(process.platform==="linux"&&!o.spawn&&o.serviceCgroup===void 0)try{u=r.readFileSync("/proc/self/cgroup","utf8")}catch{}if(v(o.platform??process.platform,u)){const i=(o.systemdRun??j)("systemd-run",["--user","--quiet","--collect",`--unit=shennian-updater-${t.jobId}`,"--property=Type=exec","--property=KillMode=process",`--setenv=PATH=${process.env.PATH??"/usr/local/bin:/usr/bin:/bin"}`,"--",t.nodeExecutable,n.workerPath,c],{encoding:"utf8",stdio:"pipe",timeout:1e4});if(i.error||i.status!==0)throw new Error("updater_service_launch_failed");return{job:t,process:null}}const m=(o.spawn??I)(t.nodeExecutable,[n.workerPath,c],{detached:!0,stdio:"ignore",windowsHide:!0});return m.once?.("error",i=>{w(t.resultPath,{protocolVersion:S,jobId:t.jobId,state:"failed",fromVersion:t.fromVersion,targetVersion:t.targetVersion,errorCode:"internal_error",errorSummary:i instanceof Error?i.message.slice(0,500):"updater_spawn_failed",updatedAt:new Date().toISOString()}),h()}),m.unref(),{job:t,process:m}}catch(u){l!==null&&r.closeSync(l),h();try{r.unlinkSync(c)}catch{}throw u}}async function L(e,o=8e3){const a=Date.now()+o;for(;Date.now()<a;){let n=null;try{n=JSON.parse(r.readFileSync(e.resultPath,"utf8"))}catch{}if(n?.jobId===e.jobId&&n.state==="waiting_for_daemon")return;if(n?.jobId===e.jobId&&n.state==="failed")throw new Error(n.errorCode??"updater_not_ready");await new Promise(t=>setTimeout(t,50))}throw new Error("updater_readiness_timeout")}function q(e){w(`${e.resultPath}.handoff`,{jobId:e.jobId})}function M(e,o,a=f("runtime","updater")){if(!/^[A-Za-z0-9_-]{1,128}$/.test(e))return!1;const n=d.join(a,"results",`${e}.json`);try{const t=r.lstatSync(n);if(t.isSymbolicLink()||!t.isFile())return!1;const c=JSON.parse(r.readFileSync(n,"utf8"));return c.protocolVersion!==S||c.jobId!==e||c.state!=="starting"||c.targetVersion!==o?!1:(w(n,{...c,state:c.operation==="rollback"?"rolled_back":"healthy",effectiveVersion:o,updatedAt:new Date().toISOString()}),!0)}catch{return!1}}export{q as confirmUpdaterHandoff,$ as createUpdaterJob,R as launchUpdater,M as markUpdaterHealthy,v as needsSystemdUpdater,L as waitForUpdaterReady,O as writeUpdaterJob};
1
+ import r from"node:fs";import d from"node:path";import{randomUUID as y}from"node:crypto";import{spawn as I,spawnSync as j}from"node:child_process";import{resolveShennianPath as f}from"../config/index.js";import{findPackageJson as P,resolveNpmGlobalPrefixFromPackageJson as _}from"./engine.js";import{ensureUpdaterInstalled as E}from"./updater-bootstrap.js";import{resolveNpmCommand as O}from"./npm-command.js";import{UPDATER_PROTOCOL_VERSION as S}from"./updater-protocol.js";function w(e,o){r.mkdirSync(d.dirname(e),{recursive:!0,mode:448});const a=`${e}.tmp-${process.pid}-${y()}`;try{r.writeFileSync(a,`${JSON.stringify(o,null,2)}
2
+ `,{mode:384,flag:"wx"});const n=r.openSync(a,process.platform==="win32"?"r+":"r");try{r.fsyncSync(n)}finally{r.closeSync(n)}if(r.renameSync(a,e),process.platform!=="win32"){const t=r.openSync(d.dirname(e),"r");try{r.fsyncSync(t)}finally{r.closeSync(t)}}}finally{try{r.unlinkSync(a)}catch{}}}function J(e){const o=e.root??f("runtime","updater"),a=P(),n=e.npmPrefix??(a?_(a):null);if(!n)throw new Error("Cannot determine the active npm prefix");const t=e.jobId??y();return{protocolVersion:S,jobId:t,fromVersion:e.fromVersion,targetVersion:e.targetVersion,npmPrefix:n,npmExecutable:e.npmExecutable??O(e.nodeExecutable??process.execPath),nodeExecutable:e.nodeExecutable??process.execPath,packageName:"shennian",operation:e.operation??"upgrade",oldDaemonPid:e.oldDaemonPid,platform:process.platform,createdAt:(e.now??new Date).toISOString(),daemonExitTimeoutMs:15e3,commandTimeoutMs:12e4,resultPath:d.join(o,"results",`${t}.json`),attemptPath:f("upgrade-attempt.json"),...e.requireHandoffAck?{requireHandoffAck:!0}:{},...e.rollbackJobId?{rollbackJobId:e.rollbackJobId}:{},...e.isolatedDaemonStart||process.env.SHENNIAN_DAEMON_SUPERVISOR==="docker"?{isolatedDaemonStart:!0}:{}}}function $(e,o=f("runtime","updater")){const a=d.join(o,"jobs");r.mkdirSync(a,{recursive:!0,mode:448});const n=d.join(a,`${e.jobId}.json`),t=`${n}.tmp-${process.pid}-${y()}`;try{r.writeFileSync(t,`${JSON.stringify(e,null,2)}
3
+ `,{mode:384,flag:"wx"});const c=r.openSync(t,process.platform==="win32"?"r+":"r");try{r.fsyncSync(c)}finally{r.closeSync(c)}if(r.renameSync(t,n),process.platform!=="win32"){const s=r.openSync(a,"r");try{r.fsyncSync(s)}finally{r.closeSync(s)}}return n}finally{try{r.unlinkSync(t)}catch{}}}function v(e,o){return e==="linux"&&/(?:^|\/)shennian\.service(?:\n|$)/m.test(o.trim())}function R(e,o={}){const a=o.root??e.root??f("runtime","updater"),n=o.readiness??E({root:a}),t=J({...e,root:a}),c=$(t,a),s=d.join(a,"updater.lock");r.mkdirSync(a,{recursive:!0,mode:448});let l=null,p=!1;const h=()=>{if(p){try{JSON.parse(r.readFileSync(s,"utf8")).jobId===t.jobId&&r.unlinkSync(s)}catch{}p=!1}};try{try{l=r.openSync(s,"wx",384),p=!0}catch(i){if(i.code!=="EEXIST")throw i;let b=!1;try{const k=JSON.parse(r.readFileSync(s,"utf8")),g=o.isAlive??(x=>{try{return process.kill(x,0),!0}catch{return!1}});b=!!(k.pid&&g(k.pid))}catch{}if(b)throw new Error("upgrade_locked");r.rmSync(s,{force:!0}),l=r.openSync(s,"wx",384),p=!0}r.writeFileSync(l,JSON.stringify({jobId:t.jobId,phase:"handoff",pid:process.pid,createdAt:(o.now??Date.now)()})),r.closeSync(l),l=null;let u=o.serviceCgroup??"";if(process.platform==="linux"&&!o.spawn&&o.serviceCgroup===void 0)try{u=r.readFileSync("/proc/self/cgroup","utf8")}catch{}if(v(o.platform??process.platform,u)){const i=(o.systemdRun??j)("systemd-run",["--user","--quiet","--collect",`--unit=shennian-updater-${t.jobId}`,"--property=Type=exec","--property=KillMode=process",`--setenv=PATH=${process.env.PATH??"/usr/local/bin:/usr/bin:/bin"}`,"--",t.nodeExecutable,n.workerPath,c],{encoding:"utf8",stdio:"pipe",timeout:1e4});if(i.error||i.status!==0)throw new Error("updater_service_launch_failed");return{job:t,process:null}}const m=(o.spawn??I)(t.nodeExecutable,[n.workerPath,c],{detached:!0,stdio:"ignore",windowsHide:!0});return m.once?.("error",i=>{w(t.resultPath,{protocolVersion:S,jobId:t.jobId,state:"failed",fromVersion:t.fromVersion,targetVersion:t.targetVersion,errorCode:"internal_error",errorSummary:i instanceof Error?i.message.slice(0,500):"updater_spawn_failed",updatedAt:new Date().toISOString()}),h()}),m.unref(),{job:t,process:m}}catch(u){l!==null&&r.closeSync(l),h();try{r.unlinkSync(c)}catch{}throw u}}async function L(e,o=8e3){const a=Date.now()+o;for(;Date.now()<a;){let n=null;try{n=JSON.parse(r.readFileSync(e.resultPath,"utf8"))}catch{}if(n?.jobId===e.jobId&&n.state==="waiting_for_daemon")return;if(n?.jobId===e.jobId&&n.state==="failed")throw new Error(n.errorCode??"updater_not_ready");await new Promise(t=>setTimeout(t,50))}throw new Error("updater_readiness_timeout")}function M(e){w(`${e.resultPath}.handoff`,{jobId:e.jobId})}function q(e,o,a=f("runtime","updater")){if(!/^[A-Za-z0-9_-]{1,128}$/.test(e))return!1;const n=d.join(a,"results",`${e}.json`);try{const t=r.lstatSync(n);if(t.isSymbolicLink()||!t.isFile())return!1;const c=JSON.parse(r.readFileSync(n,"utf8"));return c.protocolVersion!==S||c.jobId!==e||c.state!=="starting"||c.targetVersion!==o?!1:(w(n,{...c,state:c.operation==="rollback"?"rolled_back":"healthy",effectiveVersion:o,updatedAt:new Date().toISOString()}),!0)}catch{return!1}}export{M as confirmUpdaterHandoff,J as createUpdaterJob,R as launchUpdater,q as markUpdaterHealthy,v as needsSystemdUpdater,L as waitForUpdaterReady,$ as writeUpdaterJob};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.5.23",
3
+ "version": "0.5.25",
4
4
  "description": "Shennian \u2014 AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {