shennian 0.4.80 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/publish-build-manifest.json +71 -51
- package/dist/src/agent-env.js +3 -3
- package/dist/src/commands/daemon-service-spec.d.ts +0 -2
- package/dist/src/commands/daemon-service-spec.js +1 -1
- package/dist/src/commands/daemon-state.js +1 -1
- package/dist/src/commands/daemon-windows.d.ts +0 -3
- package/dist/src/commands/daemon-windows.js +12 -12
- package/dist/src/commands/daemon.d.ts +1 -0
- package/dist/src/commands/daemon.js +16 -16
- package/dist/src/commands/pair.js +7 -7
- package/dist/src/commands/upgrade.js +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/native-fusion/file-identity.d.ts +9 -0
- package/dist/src/native-fusion/file-identity.js +1 -0
- package/dist/src/native-fusion/local-repository.d.ts +6 -0
- package/dist/src/native-fusion/local-repository.js +1 -1
- package/dist/src/native-fusion/service.js +2 -2
- package/dist/src/native-fusion/session-native-sources.d.ts +24 -0
- package/dist/src/native-fusion/session-native-sources.js +1 -0
- package/dist/src/runtime-distribution.d.ts +2 -8
- package/dist/src/runtime-distribution.js +1 -1
- package/dist/src/session/deletion-synchronizer.d.ts +9 -0
- package/dist/src/session/deletion-synchronizer.js +1 -0
- package/dist/src/session/manager.d.ts +1 -1
- package/dist/src/session/manager.js +1 -1
- package/dist/src/session/queue-control.d.ts +27 -0
- package/dist/src/session/queue-control.js +1 -0
- package/dist/src/session/queue.d.ts +2 -1
- package/dist/src/session/queue.js +1 -1
- package/dist/src/upgrade/engine.js +2 -2
- package/dist/src/upgrade/scheduler.js +1 -1
- package/dist/src/upgrade/updater-launcher.js +3 -3
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import a from"chalk";import{
|
|
2
|
-
`)}function
|
|
3
|
-
<string>${I(
|
|
4
|
-
`),
|
|
1
|
+
import a from"chalk";import{readCliDistribution as ae}from"../runtime-distribution.js";import s from"node:fs";import l from"node:path";import g from"node:os";import{execSync as c,spawn as ce}from"node:child_process";import{randomUUID as ue}from"node:crypto";import{fileURLToPath as le}from"node:url";import{loadConfig as de,resolveShennianPath as v,saveConfig as pe}from"../config/index.js";import{buildWindowsLauncherCommand as me,buildWindowsScheduledTaskXml as fe,buildWindowsStartupVbs as Se,escapeXml as I,isWindowsCmdScript as ge,quoteCmdArg as O,quoteSystemdArg as he,registerWindowsScheduledTask as ye,suspendWindowsScheduledTask as we}from"./daemon-windows.js";import{isDaemonReadyStatus as ve,waitForDaemonReadyStatus as A}from"./daemon-start-wait.js";import{getCurrentVersion as W}from"../upgrade/engine.js";import{buildWindowsLauncherCommand as gn,buildWindowsScheduledTaskCommands as hn,buildWindowsScheduledTaskSuspendCommands as yn,buildWindowsScheduledTaskXml as wn,buildWindowsStartupVbs as vn,suspendWindowsScheduledTask as kn}from"./daemon-windows.js";import{inferDaemonLauncherFromCommand as bn,isShennianRunServiceCommand as Pn}from"./daemon-process.js";import{SHENNIAN_DIR as U,PID_FILE as E,LOG_FILE as d,REMOTE_ACCESS_DISABLED_FILE as B,readPidRecord as x,clearDaemonLauncher as ke,shouldDiscoverGlobalDaemonProcesses as De,getPlatform as f,isRunning as k,waitForPidExitSync as F,isRemoteAccessDisabled as be,clearRemoteAccessDisabled as Pe,recordStartedDaemon as $e,findRunningDaemonProcessIds as Ie,shouldInstallPlatformService as R,getDaemonStatus as u}from"./daemon-state.js";import{isEphemeralCliPath as Ae,resolveServiceLaunchSpec as Ee,captureEnvForService as H,saveEnvSnapshot as xe}from"./daemon-service-spec.js";import{isRemoteAccessDisabled as In,clearRemoteAccessDisabled as An,getCurrentProcessDaemonLauncher as En,createDaemonInstanceId as xn,writeDaemonPid as Rn,clearDaemonPidIfOwner as Ln,writeDaemonLauncher as _n,clearDaemonLauncher as jn,recordStartedDaemon as Nn,findRunningDaemonProcessIds as Tn,isIsolatedDaemonE2eInstance as Cn,shouldDiscoverGlobalDaemonProcesses as On,shouldInstallPlatformService as Wn,getDaemonStatus as Un}from"./daemon-state.js";import{isSafeSnapshotEnvKey as Fn,isEphemeralCliPath as Hn,resolveServiceLaunchSpec as Mn,captureEnvForService as Gn,saveEnvSnapshot as Vn}from"./daemon-service-spec.js";const Re=l.dirname(le(import.meta.url)),M=l.resolve(Re,"../../bin/shennian.js"),G=process.execPath;function V(e,n){const t=l.dirname(e),r=`${e}.${process.pid}.${ue()}.tmp`;s.mkdirSync(t,{recursive:!0});let o=null;try{o=s.openSync(r,"w",384),s.writeSync(o,n,void 0,"utf8"),s.fsyncSync(o),s.closeSync(o),o=null,s.renameSync(r,e);try{const i=s.openSync(t,"r");try{s.fsyncSync(i)}finally{s.closeSync(i)}}catch{}}finally{o!==null&&s.closeSync(o);try{s.unlinkSync(r)}catch{}}}function Le(e={}){const n=e.disabledAt??new Date().toISOString(),t=e.markerFile??B,r=e.platform??f(),o=e.servicePresent??(R()&&N(r)),i=e.execCommand??(m=>{c(m,{stdio:"pipe",windowsHide:!0})});if(V(t,n),!o)return{disabledAt:n,autoStartDisabled:!0};try{return i(r==="darwin"?`launchctl disable "${_()}"`:r==="linux"?"systemctl --user disable shennian":`schtasks /change /tn "${w}" /disable`),{disabledAt:n,autoStartDisabled:!0}}catch(m){return{disabledAt:n,autoStartDisabled:!1,autoStartError:m instanceof Error?m.message:String(m)}}}function _e(e={}){const n=e.platform??f();if(!(e.servicePresent??(R()&&N(n))))return{serviceStopped:!0};const r=e.execCommand??(o=>{c(o,{stdio:"pipe",windowsHide:!0})});try{return r(n==="darwin"?`launchctl bootout "${_()}"`:n==="linux"?"systemctl --user stop shennian":`schtasks /end /tn "${w}"`),{serviceStopped:!0}}catch(o){return{serviceStopped:!1,error:o instanceof Error?o.message:String(o)}}}function p(e){process.stdout.write(`${JSON.stringify(e,null,2)}
|
|
2
|
+
`)}function J(e){const n=e?.trim();if(!n)return;const t=de();t.serverUrl=n,pe(t)}function K(e){return e?.trim()||process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim()||void 0}const L="com.shennian.agent",S=l.join(g.homedir(),"Library/LaunchAgents",`${L}.plist`),q=v("Shennian.app"),je=l.join(g.homedir(),"AppData","Roaming","Microsoft","Windows","Start Menu","Programs","Startup"),D=v("autostart.cmd"),b=v("autostart.vbs"),P=v("autostart.xml"),Ne=l.join(je,"ShennianAgent.vbs");function X(e){const n=f()==="win32"?`where ${e}`:`command -v ${e}`;try{return c(n,{stdio:["ignore","pipe","pipe"],encoding:"utf-8",windowsHide:!0}).split(/\r?\n/).map(o=>o.trim()).find(Boolean)??null}catch{return null}}function Te(){const e=f()==="win32"?"npx.cmd":"npx",n=l.join(l.dirname(G),e);return s.existsSync(n)?n:X("npx")}function h(){return Ee({nodeExec:G,scriptPath:M,shennianCommandPath:X("shennian"),npxPath:Te()})}function Y(){try{c(`schtasks /delete /tn "${w}" /f`,{stdio:"pipe",windowsHide:!0})}catch{}}function z(){try{s.unlinkSync(Ne)}catch{}}function Ce(){const e=process.env.USERDOMAIN?.trim(),n=process.env.COMPUTERNAME?.trim(),t=process.env.USERNAME?.trim()||g.userInfo().username;return e&&e.toUpperCase()!=="WORKGROUP"?`${e}\\${t}`:n?`${n}\\${t}`:t}function Oe(e){if(f()==="win32"&&ge(e.command)){const n=[e.command,...e.args].map(O).join(" ");return{command:process.env.ComSpec||"cmd.exe",args:["/d","/s","/c",`"${n}"`],windowsVerbatimArguments:!0}}return{command:e.command,args:e.args}}function We(e,n,t=process.env){return{detached:!0,stdio:["ignore",n,n],env:t,windowsHide:!0,...e.windowsVerbatimArguments?{windowsVerbatimArguments:!0}:{}}}function Ue(){const e=h();s.writeFileSync(D,me(e,d)),s.writeFileSync(b,Se(D)),z(),Y();const n=fe({userId:Ce(),command:l.join(process.env.SystemRoot||"C:\\Windows","System32","wscript.exe"),arguments:[O(b)]});return s.writeFileSync(P,`\uFEFF${n}`,"utf16le"),ye({taskName:w,xmlPath:P},t=>{c(t,{stdio:"pipe",windowsHide:!0})})}function Be(){we(w,e=>{c(e,{stdio:"pipe",windowsHide:!0})})}function Fe(e={}){const n=H(),t=e.launch??h(),r=Object.entries(n).map(([i,m])=>` <key>${I(i)}</key>
|
|
3
|
+
<string>${I(m)}</string>`).join(`
|
|
4
|
+
`),o=[t.command,...t.args].map(i=>` <string>${I(i)}</string>`).join(`
|
|
5
5
|
`);return`<?xml version="1.0" encoding="UTF-8"?>
|
|
6
6
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
7
7
|
<plist version="1.0">
|
|
@@ -10,11 +10,11 @@ import a from"chalk";import{isDesktopDistribution as ie,readCliDistribution as a
|
|
|
10
10
|
<string>${L}</string>
|
|
11
11
|
<key>ProgramArguments</key>
|
|
12
12
|
<array>
|
|
13
|
-
${
|
|
13
|
+
${o}
|
|
14
14
|
</array>
|
|
15
15
|
<key>EnvironmentVariables</key>
|
|
16
16
|
<dict>
|
|
17
|
-
${
|
|
17
|
+
${r}
|
|
18
18
|
</dict>
|
|
19
19
|
<key>RunAtLoad</key>
|
|
20
20
|
<true/>
|
|
@@ -23,24 +23,24 @@ ${t}
|
|
|
23
23
|
<key>ThrottleInterval</key>
|
|
24
24
|
<integer>5</integer>
|
|
25
25
|
<key>StandardOutPath</key>
|
|
26
|
-
<string>${
|
|
26
|
+
<string>${d}</string>
|
|
27
27
|
<key>StandardErrorPath</key>
|
|
28
|
-
<string>${
|
|
28
|
+
<string>${d}</string>
|
|
29
29
|
</dict>
|
|
30
|
-
</plist>`}function
|
|
31
|
-
`),
|
|
30
|
+
</plist>`}function Q(e=q){s.rmSync(e,{recursive:!0,force:!0})}function He(e=f()){return e!=="darwin"?null:"macOS may show \u201CNode.js Foundation\u201D under Allow in the Background. This is the Node.js runtime used by Shennian Client."}const y=l.join(g.homedir(),".config/systemd/user/shennian.service");function Me(){const e=H(),n=h(),t=Object.entries(e).map(([o,i])=>`Environment=${o}=${i}`).join(`
|
|
31
|
+
`),r=[n.command,...n.args].map(he).join(" ");return`[Unit]
|
|
32
32
|
Description=Shennian Agent Daemon
|
|
33
33
|
After=network.target
|
|
34
34
|
|
|
35
35
|
[Service]
|
|
36
|
-
${
|
|
37
|
-
ExecStart=${
|
|
36
|
+
${t}
|
|
37
|
+
ExecStart=${r}
|
|
38
38
|
Restart=on-failure
|
|
39
39
|
RestartSec=10
|
|
40
|
-
StandardOutput=append:${
|
|
41
|
-
StandardError=append:${
|
|
40
|
+
StandardOutput=append:${d}
|
|
41
|
+
StandardError=append:${d}
|
|
42
42
|
|
|
43
43
|
[Install]
|
|
44
|
-
WantedBy=default.target`}const w="ShennianAgent";function Ge(e){c(e,{stdio:"pipe",windowsHide:!0})}function
|
|
44
|
+
WantedBy=default.target`}const w="ShennianAgent";function Ge(e){c(e,{stdio:"pipe",windowsHide:!0})}function Z(){const e=process.getuid?.();if(typeof e=="number")return e;const n=g.userInfo();return typeof n.uid=="number"?n.uid:n.username}function _(e=Z()){return`gui/${e}/${L}`}function Ve(e=Ge){const n=`gui/${Z()}`,t=_();try{return e(`launchctl enable "${t}"`),e(`launchctl bootout "${t}" 2>/dev/null || true`),e(`launchctl bootstrap "${n}" "${S}"`),e(`launchctl kickstart -k "${t}"`),e(`launchctl print "${t}"`),!0}catch{try{return e(`launchctl enable "${t}"`),e(`launchctl unload "${S}" 2>/dev/null || true`),e(`launchctl load -w "${S}"`),e(`launchctl kickstart -k "${t}"`),e(`launchctl print "${t}"`),!0}catch{return!1}}}function ee(e={}){s.mkdirSync(U,{recursive:!0});const n=u({cleanupStale:!0}),t=$(n);if(t)throw new Error(t);if(n.pid!==null&&n.running&&!j(n)){e.quiet||console.log(a.green(`\u2713 Background service already running (PID ${n.pid})`));return}ne(n);const r=s.openSync(d,"a"),o=Oe(h()),i=ce(o.command,o.args,We(o,r));i.unref(),s.closeSync(r),$e(i.pid),e.quiet||(console.log(a.green(`\u2713 Background service started (PID ${i.pid})`)),console.log(a.gray(` Logs: ${d}`)))}function j(e,n=x(),t=W()){return e.pid===null||!e.running||e.launcher!=="global-cli"?!1:e.adopted===!0||n?.version!==t}function $(e){return e.pid===null||!e.running||e.launcher==="global-cli"?null:`unsupported_daemon_source:${e.launcher}`}function N(e=f()){switch(e){case"darwin":return s.existsSync(S)&&!s.existsSync(q);case"linux":return s.existsSync(y);case"win32":return s.existsSync(D)&&s.existsSync(b)&&s.existsSync(P)}}function Je(e,n,t,r=x(),o=W()){return!n.api?.trim()&&e.pid!==null&&e.running&&e.launcher==="global-cli"&&!e.stale&&!e.remoteAccessDisabled&&t&&!j(e,r,o)}function ne(e=u({cleanupStale:!0})){if(!j(e)||e.pid===null)return;let n;try{process.kill(e.pid,"SIGTERM"),F(e.pid,5e3)||(process.kill(e.pid,"SIGKILL"),F(e.pid,2e3))}catch(t){n=t}if(k(e.pid)){const t=n instanceof Error?n.message:"daemon_stop_timeout";throw new Error(`daemon_replacement_failed: ${t}`)}try{x()?.pid===e.pid&&s.unlinkSync(E)}catch{}}function te(e={}){if(s.mkdirSync(U,{recursive:!0}),!R())return!1;const n=f();switch(h().mode==="direct"&&Ae(M)&&console.warn(a.yellow("\u26A0 Warning: Current CLI path is temporary (npx). Auto-start may not work after reboot.\n Run `npm install -g shennian@latest` for reliable auto-start.")),n){case"darwin":{const r=l.dirname(S);s.mkdirSync(r,{recursive:!0}),Q(),s.writeFileSync(S,Fe());const o=Ve(),i=He(n);return!e.quiet&&i&&console.log(a.gray(`\u2139 ${i}`)),o}case"linux":{const r=l.dirname(y);s.mkdirSync(r,{recursive:!0}),s.writeFileSync(y,Me());try{c("systemctl --user daemon-reload && systemctl --user enable shennian",{stdio:"pipe",windowsHide:!0})}catch{return!1}try{c(`loginctl enable-linger ${g.userInfo().username}`,{stdio:"pipe",windowsHide:!0})}catch{}try{return c("systemctl --user restart shennian",{stdio:"pipe",windowsHide:!0}),!0}catch{return!1}}case"win32":return Ue()}}function Ke(){const n=u({cleanupStale:!0}).pid;if(n===null)return{};if(!k(n)){try{s.unlinkSync(E)}catch{}return{stalePid:n}}try{return process.kill(n,"SIGTERM"),{stoppedPid:n}}catch(t){return{error:t instanceof Error?t.message:String(t)}}}async function re(e=5e3){const n=Ke();if(!n.stoppedPid)return n;if(!await C(n.stoppedPid,e))try{if(process.kill(n.stoppedPid,"SIGKILL"),!await C(n.stoppedPid,2e3))return{...n,error:"daemon_stop_timeout"}}catch(r){return{...n,error:r instanceof Error?r.message:String(r)}}try{s.unlinkSync(E)}catch{}if(ke(),De()){for(const r of Ie())if(r!==n.stoppedPid)try{process.kill(r,"SIGTERM"),await C(r,2e3)}catch{}}return n}function qe(e={}){const n=be();try{const t=u({cleanupStale:!0}),r=$(t);if(r)throw new Error(r);J(K(e.api)),Pe(),ne(t),te({quiet:e.json})||ee({quiet:e.json});const i=t.running?t.instanceId:void 0,m=A(()=>u({cleanupStale:!0}),{excludedInstanceId:i});if(!ve(m,i))throw new Error("daemon_start_failed");e.json?p(m):console.log(a.green("\u2713 Background service started"))}catch(t){n&&V(B,new Date().toISOString()),process.exitCode=1;const r=t instanceof Error?t.message:String(t);e.json?p({...u(),error:r}):console.error(a.red(`Background service did not start: ${r}`))}}async function Xe(e={}){const n=Le(),t=_e(),r=await re(),o=u({cleanupStale:!0}),i=r.error??n.autoStartError??t.error??(o.running?"daemon_stop_failed":o.remoteAccessDisabled?void 0:"remote_access_pause_failed");if(i&&(process.exitCode=1),e.json){p(i?{...o,error:i}:o);return}i?console.error(a.red(`\u2717 Failed to stop completely: ${i}`)):console.log(a.green("\u2713 Background service stopped"))}function oe(e){const n=u({cleanupStale:!0});if(Je(n,e,N())){e.json?p(n):console.log(a.green(`\u2713 Background service already running (PID ${n.pid})`));return}xe(),qe(e)}async function T(e={}){const n=u({cleanupStale:!0}),t=$(n);if(t){process.exitCode=1,e.json?p({...n,error:t}):console.error(a.red(`Background service was not stopped: ${t}`));return}await Xe(e)}async function C(e,n=5e3){const t=Date.now();for(;Date.now()-t<n;){if(!k(e))return!0;await new Promise(r=>setTimeout(r,100))}return!k(e)}async function Ye(e={}){const n=u({cleanupStale:!0}),t=$(n);if(t){process.exitCode=1,e.json?p({...n,error:t}):console.error(a.red(`Background service was not restarted: ${t}`));return}if(n.remoteAccessDisabled){e.json?p({...n,error:"remote_access_disabled_use_shennian_start"}):console.error(a.yellow("Remote access is paused. Run `shennian start` on this computer to resume."));return}J(K(e.api));const r=f();r==="win32"&&Be();const o=await re();if(o.error){e.json?p({...u(),error:o.error}):console.error(a.red(`\u2717 Failed to stop: ${o.error}`));return}if(o.stoppedPid!==void 0?e.json||console.log(a.green(`\u2713 Background service stopped (PID ${o.stoppedPid})`)):o.stalePid!==void 0?e.json||console.log(a.yellow(`\u26A0 Service process no longer exists, cleaned up stale PID ${o.stalePid}`)):e.json||console.log(a.gray("\u25CF Background service not running, starting it now")),(r==="linux"||r==="win32")&&te()){e.json&&p(A(()=>u({cleanupStale:!0})));return}ee({quiet:e.json}),e.json&&p(A(()=>u({cleanupStale:!0})))}function se(e={}){const n={...u(),client:ae()};if(e.json){p(n);return}if(n.pid===null){console.log(a.gray("\u25CF Background service not running"));return}n.running?(console.log(a.green(`\u25CF Background service running (PID ${n.pid})`)),console.log(a.gray(` Logs: ${d}`))):console.log(a.yellow(`\u25CF Background service stopped (PID ${n.pid} is stale)`))}function ie(e){if(!s.existsSync(d)){console.log(a.gray("No logs yet"));return}try{const n=c(g.platform()==="win32"?`powershell Get-Content -Tail ${e.lines} "${d}"`:`tail -n ${e.lines} "${d}"`,{encoding:"utf-8",windowsHide:!0});process.stdout.write(n)}catch{const n=s.readFileSync(d,"utf-8").split(`
|
|
45
45
|
`).slice(-e.lines);console.log(n.join(`
|
|
46
|
-
`))}}async function ze(){switch(await
|
|
46
|
+
`))}}async function ze(){switch(await T(),f()){case"darwin":{if(s.existsSync(S)){try{c(`launchctl unload -w "${S}"`,{stdio:"pipe",windowsHide:!0})}catch{}s.unlinkSync(S)}Q(),console.log(a.green("\u2713 Auto-start uninstalled (launchd)"));break}case"linux":{try{c("systemctl --user disable --now shennian",{stdio:"pipe",windowsHide:!0})}catch{}if(s.existsSync(y)){s.unlinkSync(y);try{c("systemctl --user daemon-reload",{stdio:"pipe",windowsHide:!0})}catch{}}console.log(a.green("\u2713 Auto-start uninstalled (systemd)"));break}case"win32":{Y();try{s.unlinkSync(D)}catch{}try{s.unlinkSync(b)}catch{}try{s.unlinkSync(P)}catch{}z(),console.log(a.green("\u2713 Auto-start uninstalled (Task Scheduler)"));break}}}function mn(e){e.command("start").description("Start the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>oe(r)),e.command("stop").description("Stop the background service").option("--json","Print machine-readable status JSON").action(r=>T(r)),e.command("status").description("Show background service status").option("--json","Print machine-readable status JSON").action(r=>se(r)),e.command("logs").description("Show recent logs").option("-n, --lines <n>","Number of lines","50").action(r=>ie({lines:parseInt(r.lines,10)}));const n=e.command("daemon",{hidden:!0}).description("Deprecated: use top-level start/stop/status/logs"),t=(r,o)=>{o||console.error(a.yellow(`\u26A0 Deprecated command. Use \`shennian ${r}\` instead.`))};n.command("start").description("Start the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>{t("start",r.json),oe(r)}),n.command("stop").description("Stop the background service").option("--json","Print machine-readable status JSON").action(async r=>{t("stop",r.json),await T(r)}),n.command("restart").description("Restart the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>{t("stop && shennian start",r.json),Ye(r)}),n.command("status").description("Show background service status").option("--json","Print machine-readable status JSON").action(r=>{t("status",r.json),se(r)}),n.command("logs").description("Show recent logs").option("-n, --lines <n>","Number of lines","50").action(r=>{t("logs"),ie({lines:parseInt(r.lines,10)})}),n.command("uninstall").description("Uninstall auto-start service").action(ze)}export{We as buildDaemonSpawnOptions,Fe as buildMacLaunchdPlist,Me as buildSystemdUnit,gn as buildWindowsLauncherCommand,hn as buildWindowsScheduledTaskCommands,yn as buildWindowsScheduledTaskSuspendCommands,wn as buildWindowsScheduledTaskXml,vn as buildWindowsStartupVbs,Je as canReuseRunningDaemonForStart,Gn as captureEnvForService,jn as clearDaemonLauncher,Ln as clearDaemonPidIfOwner,An as clearRemoteAccessDisabled,xn as createDaemonInstanceId,Tn as findRunningDaemonProcessIds,En as getCurrentProcessDaemonLauncher,Un as getDaemonStatus,_ as getLaunchdServiceTarget,He as getMacBackgroundItemNotice,$ as getUnsupportedDaemonSourceError,bn as inferDaemonLauncherFromCommand,te as installService,N as isAutoStartServicePresent,Hn as isEphemeralCliPath,Cn as isIsolatedDaemonE2eInstance,In as isRemoteAccessDisabled,Fn as isSafeSnapshotEnvKey,Pn as isShennianRunServiceCommand,Le as prepareRemoteAccessPause,Nn as recordStartedDaemon,mn as registerDaemonCommand,Ve as reloadLaunchdAgent,Q as removeLegacyMacBackgroundApp,Mn as resolveServiceLaunchSpec,Vn as saveEnvSnapshot,On as shouldDiscoverGlobalDaemonProcesses,Wn as shouldInstallPlatformService,j as shouldReplaceRunningDaemon,ee as startDaemonProcess,_e as stopPreparedRemoteAccessService,kn as suspendWindowsScheduledTask,_n as writeDaemonLauncher,Rn as writeDaemonPid};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import r from"chalk";import{getDaemonInstallationId as D}from"../personal-sync/identity.js";import k from"node:os";import R from"node:readline";import{loadConfig as u,saveConfig as v}from"../config/index.js";import{detectAgents as E}from"../agents/detect.js";import{clearRemoteAccessDisabled as A,getDaemonStatus as B,startDaemonProcess as b,installService as U,saveEnvSnapshot as x}from"./daemon.js";import{waitForDaemonReadyStatus as L}from"./daemon-start-wait.js";import{detectAndChooseServer as N}from"../region.js";import{buildPairQrPayload as M,PAIR_QR_RENDER_OPTIONS as F}from"./pair-qr.js";import{BrowserPairError as J,parsePairModeChoice as q,resolvePairMode as O,runBrowserPairFlow as Q}from"./pair-browser.js";import{markAllSessionIndexesForRepublish as z}from"../session/store.js";const K=3e3,j=600*1e3;function l(o){process.stdout.write(`${JSON.stringify(o)}
|
|
2
2
|
`)}function f(o,e){l({type:"error",code:o,message:e}),process.exit(1)}function W(o){const e="\u2500".repeat(o.length+4);console.log(r.cyan(`\u250C${e}\u2510`)),console.log(r.cyan("\u2502 ")+r.bold.yellow(o)+r.cyan(" \u2502")),console.log(r.cyan(`\u2514${e}\u2518`))}async function Y(o){const e=(await import("qrcode-terminal")).default;return new Promise(n=>{e.generate(o,F,i=>{console.log(i),n()})})}async function H(o,e,n={}){const i=Date.now()+j;for(n.json&&l({type:"pair.waiting"});Date.now()<i;){await new Promise(d=>setTimeout(d,K));let a;try{a=await fetch(`${o}/api/machines/pair/status?token=${encodeURIComponent(e)}`)}catch{continue}if(a.status===410&&(n.json?f("PAIR_EXPIRED","Pairing code expired"):(console.error(r.red(`
|
|
3
|
-
\u2717 Pairing code expired. Please re-run: shennian pair`)),process.exit(1))),!a.ok)continue;const t=await a.json();if(t.status==="claimed"&&t.machineToken&&t.machineId&&t.accountNamespaceId)return n.json&&l({type:"pair.claimed"}),{machineToken:t.machineToken,machineId:t.machineId,machineName:t.machineName??"",accountNamespaceId:t.accountNamespaceId}}return null}function G(o){const e=R.createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{e.question(o,i=>{e.close(),n(i.trim().toLowerCase()==="y")})})}function C(){const o=R.createInterface({input:process.stdin,output:process.stdout});return console.log(r.white("Choose how to pair this machine:")),console.log(r.cyan(" 1. Browser authorization (recommended)")),console.log(r.gray(" 2. QR code / pairing token (servers and remote terminals)")),new Promise(e=>{o.question("Selection (1/2) [1]: ",n=>{o.close(),e(q(n))})})}async function $(o){const{serverUrl:e,machineName:n,force:i,json:a}=o,t=u();if(t.machineToken&&t.machineId&&!i){a?l({type:"done",status:"already-paired",machineId:t.machineId}):(console.log(r.green(`\u2713 Already paired (machine ID: ${t.machineId})`)),console.log(r.gray(' Run "shennian pair" to re-pair to another account')));return}const c=(await E()).map(
|
|
3
|
+
\u2717 Pairing code expired. Please re-run: shennian pair`)),process.exit(1))),!a.ok)continue;const t=await a.json();if(t.status==="claimed"&&t.machineToken&&t.machineId&&t.accountNamespaceId)return n.json&&l({type:"pair.claimed"}),{machineToken:t.machineToken,machineId:t.machineId,machineName:t.machineName??"",accountNamespaceId:t.accountNamespaceId}}return null}function G(o){const e=R.createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{e.question(o,i=>{e.close(),n(i.trim().toLowerCase()==="y")})})}function C(){const o=R.createInterface({input:process.stdin,output:process.stdout});return console.log(r.white("Choose how to pair this machine:")),console.log(r.cyan(" 1. Browser authorization (recommended)")),console.log(r.gray(" 2. QR code / pairing token (servers and remote terminals)")),new Promise(e=>{o.question("Selection (1/2) [1]: ",n=>{o.close(),e(q(n))})})}async function $(o){const{serverUrl:e,machineName:n,force:i,json:a}=o,t=u();if(t.machineToken&&t.machineId&&!i){a?l({type:"done",status:"already-paired",machineId:t.machineId}):(console.log(r.green(`\u2713 Already paired (machine ID: ${t.machineId})`)),console.log(r.gray(' Run "shennian pair" to re-pair to another account')));return}const c=(await E()).map(h=>h.type),m=k.platform(),g=m==="win32"?"windows":m==="darwin"?"macos":"linux";a||(console.log(r.bold(`
|
|
4
4
|
Shennian CLI \u2014 Pairing
|
|
5
|
-
`)),console.log(r.gray(` Machine: ${n} | OS: ${g} | Agents: ${c.join(", ")||"none"}`)),console.log());let S=o.mode??O({json:a,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY});if(S==="prompt"&&(S=await C()),S==="browser"){const
|
|
5
|
+
`)),console.log(r.gray(` Machine: ${n} | OS: ${g} | Agents: ${c.join(", ")||"none"}`)),console.log());let S=o.mode??O({json:a,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY});if(S==="prompt"&&(S=await C()),S==="browser"){const h=new AbortController,I=()=>h.abort();process.once("SIGINT",I);try{const s=await Q({serverUrl:e,machineName:n,machineOs:g,agentList:c,signal:h.signal,openUrl:o.openUrl,emit:T=>{a?l(T):T.type==="pair.browser.created"&&(console.log(r.white("Approve this machine in your browser:")),console.log(r.cyan.underline(T.url)),console.log(r.gray(`
|
|
6
6
|
If the browser did not open, copy this URL to any signed-in device.`)),console.log(r.gray("Waiting for authorization... (Ctrl+C to cancel)")))}});v({...t,machineToken:s.machineToken,machineId:s.machineId,accountNamespaceId:s.accountNamespaceId,serverUrl:e}),a?l({type:"pair.saved",machineId:s.machineId}):console.log(r.green(`
|
|
7
7
|
\u2713 Paired successfully! Machine "${s.machineName}" is now linked.`));return}catch(s){const T=s instanceof J||typeof s=="object"&&s&&"code"in s&&typeof s.code=="string"?s.code:"PAIR_BROWSER_FAILED",P=s instanceof Error?s.message:"Browser pairing failed";a&&f(T,P),console.error(r.red(`
|
|
8
|
-
\u2717 ${P}`)),process.exit(1)}finally{process.removeListener("SIGINT",I)}}let y;try{y=await fetch(`${e}/api/machines/pair/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,os:g,agentList:c,daemonInstallationId:D()})})}catch{const
|
|
9
|
-
\u2717 Timed out. Pairing code expired, please try again.`)),process.exit(1)),v({...t,machineToken:
|
|
10
|
-
\u2713 Paired successfully! Machine "${
|
|
11
|
-
Starting background service...`)),U()||b()}function
|
|
12
|
-
Starting background service...`)),U({quiet:!!e.json})||b({quiet:!!e.json}),e.json&&(L(()=>B({cleanupStale:!0})),l({type:"done",status:"paired",machineId:u().machineId}))})}export{V as registerDesktopMachine,
|
|
8
|
+
\u2717 ${P}`)),process.exit(1)}finally{process.removeListener("SIGINT",I)}}let y;try{y=await fetch(`${e}/api/machines/pair/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,os:g,agentList:c,daemonInstallationId:D()})})}catch{const h=`Network error: cannot connect to ${e}`;a&&f("NETWORK_ERROR",h),console.error(r.red(`\u2717 ${h}`)),process.exit(1)}if(!y.ok){const I=`Failed to create machine: ${(await y.json().catch(()=>({}))).error??y.statusText}`;a&&f("PAIR_CREATE_FAILED",I),console.error(r.red(`\u2717 ${I}`)),process.exit(1)}const w=await y.json(),_=M(w.pairToken);a?l({type:"pair.created",code:w.pairToken,machineId:w.machineId,expiresAt:new Date(Date.now()+j).toISOString()}):(console.log(r.white("Scan QR code or enter the pairing code in the Shennian app:")),console.log(),await Y(_),W(w.pairToken),console.log(),console.log(r.gray("Waiting for pairing... (Ctrl+C to cancel)")));const p=await H(e,w.pairToken,{json:a});p||(a&&f("PAIR_TIMEOUT","Timed out waiting for pairing"),console.error(r.red(`
|
|
9
|
+
\u2717 Timed out. Pairing code expired, please try again.`)),process.exit(1)),v({...t,machineToken:p.machineToken,machineId:p.machineId,accountNamespaceId:p.accountNamespaceId,serverUrl:e}),o.force&&z(),a&&l({type:"pair.saved",machineId:p.machineId}),a||console.log(r.green(`
|
|
10
|
+
\u2713 Paired successfully! Machine "${p.machineName}" is now linked.`))}async function V(o){const e=u(),i=(await E({serverUrl:o.serverUrl,authToken:o.authToken})).map(m=>m.type),a=k.platform(),t=a==="win32"?"windows":a==="darwin"?"macos":"linux",d=await fetch(`${o.serverUrl}/api/machines/desktop/register`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o.authToken}`},body:JSON.stringify({machineId:e.machineId,name:o.machineName,os:t,agentList:i})});if(!d.ok){const g=(await d.json().catch(()=>({}))).error??d.statusText;throw new Error(`Failed to register desktop machine: ${g}`)}const c=await d.json();if(!c.machine?.id||!c.machineToken||!c.accountNamespaceId)throw new Error("Failed to register desktop machine: missing machine token");return e.machineId=c.machine.id,e.machineToken=c.machineToken,e.accountNamespaceId=c.accountNamespaceId,e.serverUrl=o.serverUrl,v(e),{machineId:c.machine.id,machineName:c.machine.name}}async function X(o){const e=o?.trim();if(e)return e;const n=process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim();if(n)return n;const i=u().serverUrl?.trim();return i||N()}async function me(o,e){const n=u();if(!n.machineToken||!n.machineId){const a=o??await N();await $({serverUrl:a,machineName:e,force:!1})}else console.log(r.green(`\u2713 Already paired (machine ID: ${n.machineId})`));x(),A(),console.log(r.gray(`
|
|
11
|
+
Starting background service...`)),U()||b()}function he(o){o.command("desktop-register",{hidden:!0}).description("Register this machine to the authenticated account").option("--api <url>","Server URL").option("--name <name>","Machine name",k.hostname()).action(async e=>{try{const n=process.env.SHENNIAN_DESKTOP_AUTH_TOKEN;if(!n)throw new Error("Missing desktop auth token");const i=await X(e.api),a=await V({serverUrl:i,authToken:n,machineName:e.name});console.log(JSON.stringify({ok:!0,...a}))}catch(n){console.error(n instanceof Error?n.message:String(n)),process.exit(1)}}),o.command("pair").description("Pair this machine to a Shennian account (re-pair if already paired)").option("--api <url>","Server URL override").option("--server <url>","Server URL override").option("--name <name>","Machine name",k.hostname()).option("--json","Emit machine-readable JSONL events").option("--browser","Pair by approving a browser authorization request").option("--token","Pair with the existing QR code / token flow").option("--force","Force re-pair even when a machine token exists").action(async e=>{const n=u();let i;try{i=O({browser:e.browser,token:e.token,json:e.json,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY})}catch(c){const m=c instanceof Error?c.message:"Invalid pairing mode";e.json&&f("PAIR_MODE_CONFLICT",m),console.error(r.red(`\u2717 ${m}`)),process.exit(1)}if(i==="prompt"&&(i=await C()),n.machineToken&&n.machineId&&!e.force){if(e.json){l({type:"done",status:"already-paired",machineId:n.machineId});return}if(console.log(r.yellow(`Currently paired (machine ID: ${n.machineId})`)),!await G("Re-pairing will unlink the current account. Continue? (y/N) ")){console.log(r.gray("Cancelled"));return}}const a=e.server??e.api??await N(),t=!!(e.force||n.machineToken&&n.machineId);await $({serverUrl:a,machineName:e.name,force:t,json:!!e.json,mode:i}),x(),A(),e.json?l({type:"daemon.starting"}):console.log(r.gray(`
|
|
12
|
+
Starting background service...`)),U({quiet:!!e.json})||b({quiet:!!e.json}),e.json&&(L(()=>B({cleanupStale:!0})),l({type:"done",status:"paired",machineId:u().machineId}))})}export{V as registerDesktopMachine,he as registerPairCommand,$ as runPairFlow,me as runSmartStart};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import c from"chalk";import{
|
|
1
|
+
import c from"chalk";import{execSync as P,spawn as V}from"node:child_process";import{randomUUID as w}from"node:crypto";import $ from"node:os";import{createInterface as A}from"node:readline/promises";import{getCurrentVersion as y,fetchLatestVersion as k,compareVersions as v,checkForUpdate as M,acquireUpgradeLock as F,recordUpgradeFailure as O}from"../upgrade/engine.js";import{callDaemonIpc as _}from"../daemon-ipc/client.js";import{getDaemonStatus as D}from"./daemon.js";import{ensureUpdaterInstalled as U}from"../upgrade/updater-bootstrap.js";import{launchUpdater as S,createUpdaterJob as R,waitForUpdaterReady as H,confirmUpdaterHandoff as L}from"../upgrade/updater-launcher.js";import{prepareRecoveryServiceEntry as j}from"../upgrade/updater-service-entry.js";import{resolveShennianPath as q}from"../config/index.js";import{assertUpdaterAvailable as N,prepareUpdaterCandidate as C}from"../upgrade/updater-worker.js";import{getUpgradeChannel as x,setUpgradeChannel as G}from"../upgrade/channel.js";import{fetchOwnedCliArtifact as T,getCliUpdateSourceOrder as b}from"../upgrade/distribution.js";const I=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function Y(t,r){return t!=="major"||r}function ie(t={}){const r=t.platform??$.platform(),i=t.kill??process.kill,e=t.pid??process.pid,l=t.env??process.env,n=t.spawn??V,o=t.execSync??P;if(r==="win32"){const a=l.ComSpec||"cmd.exe";try{n(a,["/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{o('schtasks /run /tn "ShennianAgent"',{stdio:"pipe",windowsHide:!0})}catch{}}}else try{n("sh",["-c","sleep 2; shennian start >/dev/null 2>&1"],{detached:!0,stdio:"ignore",env:l}).unref()}catch{}i(e,"SIGTERM")}function ce(t){t.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: ${x()} (saved; no installation)`),!r.check)return}console.log(`Upgrade channel: ${x()}`);const i=y();console.log(c.gray(`Current version: ${i}`)),console.log();let e=0;const l=setInterval(()=>{process.stdout.write(`\r ${I[e%I.length]} Checking for updates...`),e++},80);let n;try{n=r.to??await k(),clearInterval(l),process.stdout.write("\r"+" ".repeat(40)+"\r")}catch(a){clearInterval(l),process.stdout.write("\r"+" ".repeat(40)+"\r"),console.error(c.red(`\u2717 Failed to check for updates: ${a instanceof Error?a.message:String(a)}`)),process.exit(1)}const o=v(i,n);if(o==="none"&&!r.to){console.log(c.green(`\u2713 Already up to date (${i})`));return}if(console.log(` Available: ${c.green(n)} ${o==="major"?c.red("(major)"):o==="minor"?c.yellow("(minor)"):c.green("(patch)")}`),console.log(),!r.check){if(o==="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 a=A({input:process.stdin,output:process.stdout}),p=await a.question(` Type ${n} to confirm this major upgrade: `);if(a.close(),p.trim()!==n){console.log(c.gray(" Upgrade cancelled."));return}}try{await _({method:"upgrade.start",params:{targetVersion:n,confirmedMajor:o==="major"||r.yes===!0},timeoutMs:1e4})}catch(a){if(D({cleanupStale:!0}).running)throw a;const m=U(),u={fromVersion:i,targetVersion:n,oldDaemonPid:null,jobId:w()},g=R(u);await C(g,{fetchOwnedArtifact:T,sourceOrder:b()}),j(g,m.workerPath),S(u,{readiness:m})}console.log(),console.log(c.green(`\u2713 Upgrade scheduled ${i} \u2192 ${n}`)),console.log(c.gray(" The independent updater will verify, roll back if needed, and restart Shennian."))}})}async function de(t,r,i,e={}){const l=s=>{t.sendEvent({type:"event",event:"upgrade",payload:s})},n=e.currentVersion??y();let o;try{l({state:"checking",machineId:"self"}),o=i??await k()}catch(s){const d=`Version check failed: ${s instanceof Error?s.message:String(s)}`;return e.suppressResponse||t.sendRes({type:"res",id:r,ok:!1,error:d}),{ok:!1,error:d}}const a=v(n,o);if(a==="none")return e.suppressResponse||t.sendRes({type:"res",id:r,ok:!0,payload:{alreadyLatest:!0,version:n}}),{ok:!0,alreadyLatest:!0,version:n};if(!Y(a,e.confirmedMajor===!0))return e.suppressResponse||t.sendRes({type:"res",id:r,ok:!1,error:"Major upgrades require explicit second confirmation"}),{ok:!1,error:"Major upgrades require explicit second confirmation"};const p=w();let m;try{m=F();try{N(q("runtime","updater"))}catch(s){throw m.release(),s}}catch(s){const d=s instanceof Error?s.message:String(s);return e.suppressResponse||t.sendRes({type:"res",id:r,ok:!1,error:d}),{ok:!1,error:d}}const u={from:n,to:o,jobId:p},g=async()=>{let s=!1;try{const d=U(),f={fromVersion:n,targetVersion:o,oldDaemonPid:process.pid,jobId:p,requireHandoffAck:!0},h=R(f);if(l({state:"downloading",machineId:"self",version:o,jobId:p}),await C(h,{fetchOwnedArtifact:T,sourceOrder:b()}),j(h,d.workerPath),S(f,{readiness:d}),await H(h),e.canHandoff&&!e.canHandoff())throw new Error("upgrade_waiting_for_idle");const E=e.prepareForUpgrade?.(p)??{activeRunCount:0};return s=!0,L(h),l({state:"restarting",machineId:"self",...u,activeRunCount:E.activeRunCount}),{ok:!0,...u,resultPath:h.resultPath}}catch(d){s&&e.cancelUpgradePreparation?.();const f=d instanceof Error?d.message:String(d);return f!=="upgrade_waiting_for_idle"&&O(o,f),l({state:"failed",machineId:"self",error:f,jobId:p}),{ok:!1,error:f}}finally{m.release()}};return e.suppressResponse||t.sendRes({type:"res",id:r,ok:!0,payload:u}),e.waitForCompletion?g():(g(),{ok:!0,...u})}async function le(t,r,i={}){const e=await M(i.currentVersion);t.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};
|
package/dist/src/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{registerScheduleCommand as Se}from"./commands/schedule.js";import{resolvePersonalScheduleContext as ye}from"./schedules/personal-context.js";import{executeServerSchedule as oe,deliverServerSchedule as we}from"./schedules/server-client.js";import{Command as Ce}from"commander";import E from"chalk";import be from"node:os";import F from"node:fs";import{loadConfig as p,saveConfig as ke,getConfigPath as Re,getShennianDir as Pe,resolveShennianPath as G,normalizeAutoUpgradePolicy as Ae}from"./config/index.js";import{CliRelayClient as Ee}from"./relay/client.js";import{collectKnownLogSecrets as ne,installCliLogRedaction as
|
|
2
|
-
`),process.exitCode=1}function Ko(a){const s=[`trigger=${a.trigger??"unknown"}`,`phase=${a.phase}`,`code=${a.code??"unknown"}`,`attempt=${a.reconnectAttempt}`];return a.reason&&s.push(`reason=${a.reason}`),a.error&&s.push(`error=${a.error}`),s.join(" ")}const i=new Ce;i.name("shennian").description("Shennian \u2014 AI Agent Control Plane").version(z),i.enablePositionalOptions(),Je(i),Ke(i),Se(i),i.option("--api <url>","Server URL override").option("--name <name>","Machine name",be.hostname()).action(async a=>{const s=p(),g=a.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??s.serverUrl??void 0;await Me(g,a.name)}),i.command("run-service",{hidden:!0}).description("(internal) Connect to relay server, called by the background service").option("--api <url>","Server URL override").action(async a=>{const s=G("env.json");try{const e=JSON.parse(F.readFileSync(s,"utf-8"));for(const[n,o]of Object.entries(e))process.env[n]||(process.env[n]=o);le()}catch{}De(ne(process.env,p()));const g=fo();ae()&&(console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`),clearInterval(g),process.exit(0));const c=xe(),y=co({lockPath:Wo,instanceId:c});y.acquired||(console.log(`[${new Date().toISOString()}] daemon already running`+(y.ownerPid?` (PID ${y.ownerPid})`:"")+", skipping duplicate start"),clearInterval(g),process.exit(0));try{const e=zo();if(e&&e!==process.pid)try{process.kill(e,0),console.log(`[${new Date().toISOString()}] daemon already running (PID ${e}), skipping duplicate start`),y.release(),process.exit(0)}catch{}}catch{}const K=Ve(process.env,fe)?[]:$e(process.pid);K.length>0&&(console.log(`[${new Date().toISOString()}] daemon already running (PID ${K[0]}), skipping duplicate start`),y.release(),process.exit(0)),se(process.pid,c,{version:z}),ce(process.pid,void 0,c),process.on("exit",()=>{te(process.pid,c),re(c)}),process.on("exit",y.release);let O=null;try{ro()||(O=to())}catch(e){console.error(`[upgrade] updater bootstrap unavailable: ${e instanceof Error?e.message:String(e)}`)}const T=await eo();if(T){O||(console.error("[upgrade] Crash-loop rollback blocked: updater bootstrap unavailable"),process.exit(1));try{ao({fromVersion:z,targetVersion:T.from,oldDaemonPid:process.pid,operation:"rollback",rollbackJobId:T.jobId},{readiness:O}),console.error(`[upgrade] New version crashed ${T.attemptCount} times; external rollback scheduled.`),process.stdin.resume();return}catch(e){console.error(`[upgrade] Crash-loop rollback could not start: ${e instanceof Error?e.message:String(e)}`),process.exit(1)}}try{Ho()}catch(e){console.error(`[personal-sync] data epoch initialization failed: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1;return}const m=p(),u=a.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??m.serverUrl??W.cn.url,h=de(),l=await so({serverUrl:u,authToken:m.machineToken??m.accessToken}),w=l.map(e=>e.type),V=N(l),f=new bo,U=new Po,C=new Oo(f),I=new ko(u),ve=new $o({actionClient:I}),Ie=new No({actionClient:I});let S=null,r;const q=Vo({authority:C,actionClient:I,attachmentCache:ve,attachmentUploader:Ie,observations:U,getMachineCredentials:()=>({machineToken:p().machineToken}),managerExecutor:e=>r.executeManagedManagerAction(e),scheduleExecutor:(e,n,o)=>oe({serverUrl:u,machineToken:p().machineToken,sessionId:e,action:n,args:o}),onPaused:({registryKey:e,sessionId:n})=>{const t=f.getByRegistryKey(e)?.binding;t&&f.setBinding({registryKey:e,bindingId:t.bindingId,roomId:t.roomId,agentIdentityId:t.agentIdentityId,agentMembershipId:t.agentMembershipId,status:"paused",activationMode:"paused"}),C.revokeSession(e),r?.discardPendingManagedRoomMessages(n)}});let L=null,j=null,H=null;const b=await me({paths:ho(),handlers:{...Io({daemonVersion:h,protocolVersion:Co,getStatus:()=>ie(),getAgents:()=>l,completeAgentAuthentication:e=>r.resumePendingAcpAuthentication(e),startUpgrade:async e=>{if(!L)throw new Error("upgrade_unavailable");return L(e)},prepareExternalUpgrade:e=>{if(!j)throw new Error("external_upgrade_unavailable");return j(e)},cancelExternalUpgrade:()=>{if(!H)throw new Error("external_upgrade_unavailable");return H()}}),...So({getStatus:()=>ie(),getSnapshot:()=>N(l).map(e=>{const n=l.find(o=>o.type===e.type);return{...e,runtimePlatform:n?.runtimePlatform,runtimeIsWsl:n?.runtimeIsWsl}}),discover:async e=>{if(!r)throw new Error("daemon_starting");const n=await yo(!0,e);if(e){const t=l.filter(A=>A.type!==e);l.splice(0,l.length,...t,...n.detectedAgents)}else l.splice(0,l.length,...n.detectedAgents);const o=N(l);return d.sendEvent({type:"event",event:"machine.agents",payload:{agentList:l.map(t=>t.type),agents:o}}),n.agents.map(t=>{const A=n.detectedAgents.find(B=>B.type===t.type);return{...t,runtimePlatform:A?.runtimePlatform,runtimeIsWsl:A?.runtimeIsWsl}})},validate:async e=>{if(!r)throw new Error("daemon_starting");return wo(e,{ignoreNativeSession:(n,o)=>R?.ignoreNativeSource(n,o)})}})}});let k;try{k=await me({paths:vo(),handlers:{"managed-room.execute":e=>q(e),"schedule.personal":async e=>{const n=e,o=ye(n?.context);if(f.getByNianSessionId(o)?.binding)throw new Error("schedule_personal_context_required");return await oe({serverUrl:u,machineToken:p().machineToken,sessionId:o,action:n.operation,args:n})}}})}catch(e){throw await b.close(),e}let Y=!1;const M=()=>{Y||(Y=!0,Promise.allSettled([b.close(),k.close()]).finally(()=>process.exit(0)))};if(process.once("SIGINT",M),process.once("SIGTERM",M),!m.machineToken){console.error('Shennian CLI is not paired. Run "shennian pair --browser" or "shennian pair --token" first.'),await Promise.allSettled([b.close(),k.close()]),process.exitCode=1;return}process.removeListener("SIGINT",M),process.removeListener("SIGTERM",M);const Q=`${Jo(u)}/relay/machine`,_=m.machineToken;if(!_)return;try{await jo({serverUrl:u,machineToken:_,machineId:m.machineId})}catch(e){console.error(`[personal-sync] machine authority verification failed: ${e instanceof Error?e.message:String(e)}`),await Promise.allSettled([b.close(),k.close()]),process.exit(1)}m.machineId&&mo(u,m.machineId),console.log(`[${new Date().toISOString()}] Connecting to ${Q}... (v${h}) agents: ${w.join(",")}`),D({level:"info",wsEvent:"daemon.start",metadata:{version:h,agents:w}});let R=null,P=null,x=0,v=oo();const Z=Ae(m.autoUpgrade),d=new Ee({serverUrl:Q,machineToken:_,cliVersion:h,agentList:w,autoUpgradePolicy:Z,sessionSyncProtocolVersion:pe,sessionSyncCapabilities:ge,daemonInstallationId:Lo(),dataEpoch:ue,updaterProtocolVersion:1,onConnected:()=>{console.log(`[${new Date().toISOString()}] \u2713 Connected`),D({level:"info",wsEvent:"daemon.connected"}),V.some(o=>o.models.length>0)&&d.sendEvent({type:"event",event:"machine.agents",payload:{agentList:w,agents:V}}),lo(l,{serverUrl:m.serverUrl??u,authToken:m.machineToken??m.accessToken}).then(o=>{JSON.stringify(o)!==JSON.stringify(V)&&d.sendEvent({type:"event",event:"machine.agents",payload:{agentList:w,agents:o}})}).catch(()=>{}),v&&d.sendEvent({type:"event",event:"upgrade",payload:{state:"recovering",machineId:"self",from:v.from,to:v.to,...v.jobId?{jobId:v.jobId}:{},activeRunCount:r.getPendingRecoveryCount()}}),$.start(),P?.start();const e=++x,n=async()=>{try{if(await r.synchronizeSessionDeletions(),await r.recoverInterruptedRuns(),v){const o=v;v=null,o.jobId&&io(o.jobId,o.to),Xe(),console.log(`[${new Date().toISOString()}] [upgrade] ${o.operation==="rollback"?"Rolled back":"Healthy"}: ${o.from} \u2192 ${o.to}`),po({kind:"upgrade",result:o.operation==="rollback"?"rolled_back":"healthy",stage:"terminal"}),d.sendEvent({type:"event",event:"upgrade",payload:{state:o.operation==="rollback"?"rolled_back":"healthy",machineId:"self",from:o.from,to:o.to,...o.jobId?{jobId:o.jobId}:{}}}),o.operation!=="rollback"&&d.sendEvent({type:"event",event:"upgrade",payload:{state:"success",machineId:"self",from:o.from,to:o.to}})}e===x&&d.getState()==="connected"&&R?.handleConnected()}catch(o){if(console.error("[session.deletions] startup catch-up failed",o),e===x&&d.getState()==="connected"){if(o instanceof Fo)return;const t=o instanceof Bo?Math.max(1e3,o.retryAfterMs):5e3;setTimeout(()=>{n()},t).unref?.()}}};n()},onDisconnected:e=>{x+=1,r?.revokeProjectFsHandles(),P?.stop(),console.log(`[${new Date().toISOString()}] \u26A0 Disconnected, reconnecting... ${Ko(e)}`),D({level:"warn",wsEvent:"daemon.disconnected",metadata:{code:e.code,reason:e.reason,error:e.error,phase:e.phase,trigger:e.trigger,reconnectAttempt:e.reconnectAttempt}})},onProjectFsCapabilityChanged:e=>{e||r?.revokeProjectFsHandles()},onReq:e=>{if(console.log(`[${new Date().toISOString()}] [req] ${e.method}`),D({level:"info",type:"ws",wsEvent:e.method,wsDirection:"in",traceId:e.traceId}),e.method==="room.message.deliver"){Eo(d,P,e);return}r.handleReq(e)}});R=go()?new uo(d):null,P=new Ao({registry:f,observations:U,actionClient:I,getMachineCredentials:()=>({machineToken:p().machineToken}),isSessionDeleted:e=>new Do().hasSession(e),onMessages:async(e,n,o,t)=>{await S?.processMessages(e,n,o,t)},onBindingPaused:e=>{C.revokeSession(e.registryKey),e.lifecycleState==="closed"?r?.retireManagedRoomSession(e.nianSessionId).catch(()=>{console.warn("[room.sync] retired session cleanup failed")}):r?.discardPendingManagedRoomMessages(e.nianSessionId)}});const $=new no({currentVersion:h,machineId:m.machineId??"unknown-machine",serverUrl:m.serverUrl??u,getIdleState:()=>r.getUpgradeIdleState(),sessionSyncProtocolVersion:pe,dataEpoch:ue,advertisedCapabilities:ge,updaterProtocolVersion:1,startUpgrade:async(e,n)=>{const{handleUpgradeStart:o}=await import("./commands/upgrade.js");await o(d,"auto-upgrade",e,{currentVersion:h,prepareForUpgrade:t=>r.prepareForUpgrade(t),cancelUpgradePreparation:()=>r.cancelUpgradePreparation(),shutdownAfterHandoff:()=>{setTimeout(()=>process.kill(process.pid,"SIGTERM"),100).unref?.()},suppressResponse:!0,waitForCompletion:!0,canHandoff:n?void 0:()=>r.getUpgradeIdleState().idle})}});$.setPolicy(Z),r=new qe(d,R,h,$,new Ro({registry:f,observations:U,bindingClient:I,getMachineCredentials:()=>{const e=p();return{machineId:e.machineId,machineToken:e.machineToken}},hostProcessIdentity:()=>`daemon:${c}`}),{prepare:()=>Ne(),isDisabled:()=>ae(),shutdownAfterResponse:()=>{setTimeout(()=>{Oe(),process.kill(process.pid,"SIGTERM")},100).unref?.()}},e=>C.revokeToken(e),q,null,Ye(_),()=>N(l)),L=async e=>{const n=e&&typeof e=="object"&&!Array.isArray(e)?e:{},o=typeof n.targetVersion=="string"?n.targetVersion:void 0,t=n.confirmedMajor===!0;return await Ge(d,`local-upgrade-${Date.now()}`,o,{currentVersion:h,confirmedMajor:t,prepareForUpgrade:B=>r.prepareForUpgrade(B),cancelUpgradePreparation:()=>r.cancelUpgradePreparation(),shutdownAfterHandoff:()=>{setTimeout(()=>process.kill(process.pid,"SIGTERM"),100).unref?.()},suppressResponse:!0})},j=e=>{const n=e&&typeof e=="object"&&!Array.isArray(e)?e:{},t=`desktop-${(typeof n.targetVersion=="string"?n.targetVersion:"unknown").replace(/[^A-Za-z0-9._-]/g,"_")}-${Date.now()}`;return{jobId:t,...r.prepareForUpgrade(t)}},H=()=>(r.cancelUpgradePreparation(),{cancelled:!0}),S=new Mo({registry:f,observations:U,enqueueConversationBatch:e=>r.enqueueManagedRoomBatch(e),loadContext:async e=>{const n=p().machineToken?.trim(),o=e.binding;if(!n||!o)throw new Error("managed_room_context_unavailable");const t=await I.status({machineToken:n,roomId:o.roomId,bindingId:o.bindingId});if(!Uo(f,e)||!To(e,t))throw new Error("managed_room_binding_unavailable");return{roomName:t.room.name,memberCount:t.room.memberCount,members:t.room.members,agentsByIdentityId:t.agent?{[t.agentIdentityId]:t.agent}:{},agentSystemPromptsByIdentityId:t.agent?{[t.agentIdentityId]:t.agent.systemPrompt??""}:{}}},issueManagedRoomContext:(e,n,o)=>{const t=p().machineToken;if(!t)throw new Error("machine_not_authorized");return C.issue({registryKey:e,machineCredential:t,activationCauseMessageId:n,allowedActions:o})},audit:new xo,resolveManagedAgentPolicy:e=>_o({...e,runtime:l.find(n=>n.type===e.registration.runtimeAdapter)})}),r.scheduleHandler=e=>we({serverUrl:u,machineToken:p().machineToken,req:e,client:d,queue:r.getScheduleQueue(),registry:f}),r.getScheduleQueue().validateScheduledTarget=async e=>{if(!e.bindingId||!e.roomId)return"ready";if(f.getByNianSessionId(e.sessionId)?.binding?.bindingId!==e.bindingId)return"ended";const o=await I.status({machineToken:p().machineToken,roomId:e.roomId,bindingId:e.bindingId});return o.status==="active"&&o.room.status==="active"?"ready":"ended"},r.setManagedRoomTurnPreparer(e=>S.prepareConversationTurn(e)),r.setManagedAgentDirectTurnPreparer((e,n)=>S.prepareDirectConversationTurn(e,n)),r.setManagedRecoveryPreparer(async e=>e.source==="managed_direct"?S.prepareDirectConversationTurn(e.sessionId,{allowedRoomActions:[]}):e.source==="manager_event"?S.prepareDirectConversationTurn(e.sessionId,{allowedRoomActions:[],managerEvent:!0}):e.source!=="managed_room"||!e.roomContextRegistryKey||!e.activationCauseMessageId||!e.rootCauseMessageId||!e.activationSequence||e.activationHopCount===null?null:S.prepareConversationTurn([{registryKey:e.roomContextRegistryKey,causeMessageId:e.activationCauseMessageId,rootCauseMessageId:e.rootCauseMessageId,sequence:e.activationSequence,hopCount:e.activationHopCount}])),F.mkdirSync(fe,{recursive:!0}),se(process.pid,c,{version:h}),ce(process.pid,void 0,c),d.connect(),process.stdin.resume();let X=!1;const ee=async()=>{if(!X){X=!0,console.log(E.gray(`
|
|
3
|
-
Disconnecting...`)),
|
|
1
|
+
import{registerScheduleCommand as Se}from"./commands/schedule.js";import{resolvePersonalScheduleContext as ye}from"./schedules/personal-context.js";import{executeServerSchedule as oe,deliverServerSchedule as we}from"./schedules/server-client.js";import{Command as Ce}from"commander";import E from"chalk";import be from"node:os";import F from"node:fs";import{loadConfig as p,saveConfig as ke,getConfigPath as Re,getShennianDir as Pe,resolveShennianPath as G,normalizeAutoUpgradePolicy as Ae}from"./config/index.js";import{CliRelayClient as Ee}from"./relay/client.js";import{collectKnownLogSecrets as ne,installCliLogRedaction as Te,sanitizeCliLogValue as De}from"./log-redaction.js";import{registerPairCommand as Ue,runSmartStart as Me}from"./commands/pair.js";import{registerLogoutCommand as _e}from"./commands/logout.js";import{clearDaemonPidIfOwner as te,createDaemonInstanceId as xe,clearDaemonLauncher as re,findRunningDaemonProcessIds as $e,isRemoteAccessDisabled as ae,prepareRemoteAccessPause as Ne,stopPreparedRemoteAccessService as Oe,isIsolatedDaemonE2eInstance as Ve,getDaemonStatus as ie,registerDaemonCommand as Le,writeDaemonPid as se,writeDaemonLauncher as ce}from"./commands/daemon.js";import{registerRuntimeCommand as je}from"./commands/runtime.js";import{registerAgentCommand as He}from"./commands/agent.js";import{registerManagerCommand as Be}from"./commands/manager.js";import{registerWorkerCommand as Fe}from"./commands/worker.js";import{handleUpgradeStart as Ge,registerUpgradeCommand as We}from"./commands/upgrade.js";import{registerToolsCommand as ze}from"./commands/tools.js";import{registerIntegrationCommand as Je}from"./commands/integration.js";import{registerRoomCommand as Ke}from"./commands/room.js";import{SessionManager as qe}from"./session/manager.js";import{createTrustedProjectFsRequestResolver as Ye}from"./session/handlers/project-fs.js";import{SERVERS as W,regionToUrl as Qe,urlToRegion as Ze}from"./region.js";import{clearUpgradeAttempt as Xe,getCurrentVersion as de,handleStartupCrashCheck as eo,readUpgradeAttempt as oo}from"./upgrade/engine.js";import{AutoUpgradeScheduler as no}from"./upgrade/scheduler.js";import{ensureUpdaterInstalled as to}from"./upgrade/updater-bootstrap.js";import{launchUpdater as ro,markUpdaterHealthy as ao}from"./upgrade/updater-launcher.js";import{detectAgents as io}from"./agents/detect.js";import{augmentProcessPath as le}from"./env-path.js";import{acquireDaemonInstanceLock as so}from"./commands/daemon-instance-lock.js";le();const z=de();import{getCachedAgentInfos as N,resolveAgentInfos as co}from"./agents/model-registry.js";import{initCliLogReporter as lo,reportLog as T,reportUpgradeAggregateMetric as mo}from"./log-reporter.js";import{isNativeFusionEnabled as po}from"./native-fusion/config.js";import{NativeSessionFusionService as go}from"./native-fusion/service.js";import{startDaemonLogRetention as uo}from"./daemon-log.js";import{startDaemonIpcServer as me}from"./daemon-ipc/server.js";import{resolveDaemonIpcPaths as fo,resolveManagedRoomIpcPaths as ho}from"./daemon-ipc/paths.js";import{createDaemonControlHandlers as vo}from"./daemon-ipc/control.js";import{createAgentReadinessHandlers as Io}from"./daemon-ipc/agent-readiness.js";import{scanMachineAgents as So}from"./session/handlers/agents.js";import{validateAgentWorkspaceConfiguration as yo}from"./session/handlers/agent-workspace-validation.js";import{DAEMON_IPC_VERSION as wo}from"./daemon-ipc/protocol.js";import{RoomSessionRegistry as Co}from"./room/session-registry.js";import{HttpRoomBindingClient as bo}from"./room/room-binding-client.js";import{TrustedManagedRoomBinder as ko}from"./room/managed-room-binding.js";import{RoomHostSessionObservationStore as Ro}from"./room/host-session-observations.js";import{RoomMirrorSyncService as Po}from"./room/mirror-sync-service.js";import{handleLiveRoomDelivery as Ao}from"./room/live-delivery.js";import{NativeSourceDeletionStore as Eo}from"./native-fusion/native-source-deletions.js";import{matchesRoomOwner as To,isCurrentRegistration as Do}from"./room/registration-authority.js";import{RoomActivationService as Uo,resolveManagedAgentPolicy as Mo}from"./room/activation-service.js";import{RoomActivationAudit as _o}from"./room/activation-audit.js";import{RoomAttachmentCache as xo}from"./room/attachment-cache.js";import{RoomAttachmentUploader as $o}from"./room/attachment-upload.js";import{ManagedRoomContextAuthority as No}from"./room/managed-context.js";import{createManagedRoomExecutor as Oo}from"./room/managed-room-ipc.js";import{SESSION_SYNC_PROTOCOL_VERSION as pe,SESSION_SYNC_ADVERTISED_CAPABILITIES as ge}from"@shennian/wire";import{getDaemonInstallationId as Vo}from"./session/store.js";import{synchronizeMachinePersonalAuthority as Lo}from"./personal-sync/machine-authority.js";import{CLI_SESSION_SYNC_DATA_EPOCH as ue,ensurePersonalDataEpoch as jo}from"./personal-sync/epoch.js";import{DeletionSyncRetryWaitError as Ho,DeletionSyncTerminalError as Bo}from"./session/deletion-sync-state.js";const fe=Pe(),Fo=G("daemon.pid"),Go=G("daemon.instance.lock");function Wo(){try{const a=F.readFileSync(Fo,"utf-8").trim();if(a.startsWith("{")){const g=JSON.parse(a),c=Number(g.pid);return Number.isInteger(c)&&c>0?c:null}const s=parseInt(a,10);return Number.isInteger(s)&&s>0?s:null}catch{return null}}function zo(a){return a.replace(/^https:\/\//,"wss://").replace(/^http:\/\//,"ws://")}function J(){process.stdout.write(`${JSON.stringify({ok:!1,error:"integration_command_retired",message:"Room MCP and Hooks were retired. Use the Shennian Skill and CLI.",nextAction:{type:"open_install_guide",url:"https://shennian.net/install.md"}})}
|
|
2
|
+
`),process.exitCode=1}function Jo(a){const s=[`trigger=${a.trigger??"unknown"}`,`phase=${a.phase}`,`code=${a.code??"unknown"}`,`attempt=${a.reconnectAttempt}`];return a.reason&&s.push(`reason=${a.reason}`),a.error&&s.push(`error=${a.error}`),s.join(" ")}const i=new Ce;i.name("shennian").description("Shennian \u2014 AI Agent Control Plane").version(z),i.enablePositionalOptions(),Je(i),Ke(i),Se(i),i.option("--api <url>","Server URL override").option("--name <name>","Machine name",be.hostname()).action(async a=>{const s=p(),g=a.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??s.serverUrl??void 0;await Me(g,a.name)}),i.command("run-service",{hidden:!0}).description("(internal) Connect to relay server, called by the background service").option("--api <url>","Server URL override").action(async a=>{const s=G("env.json");try{const e=JSON.parse(F.readFileSync(s,"utf-8"));for(const[n,o]of Object.entries(e))process.env[n]||(process.env[n]=o);le()}catch{}Te(ne(process.env,p()));const g=uo();ae()&&(console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`),clearInterval(g),process.exit(0));const c=xe(),y=so({lockPath:Go,instanceId:c});y.acquired||(console.log(`[${new Date().toISOString()}] daemon already running`+(y.ownerPid?` (PID ${y.ownerPid})`:"")+", skipping duplicate start"),clearInterval(g),process.exit(0));try{const e=Wo();if(e&&e!==process.pid)try{process.kill(e,0),console.log(`[${new Date().toISOString()}] daemon already running (PID ${e}), skipping duplicate start`),y.release(),process.exit(0)}catch{}}catch{}const K=Ve(process.env,fe)?[]:$e(process.pid);K.length>0&&(console.log(`[${new Date().toISOString()}] daemon already running (PID ${K[0]}), skipping duplicate start`),y.release(),process.exit(0)),se(process.pid,c,{version:z}),ce(process.pid,void 0,c),process.on("exit",()=>{te(process.pid,c),re(c)}),process.on("exit",y.release);let O=null;try{O=to()}catch(e){console.error(`[upgrade] updater bootstrap unavailable: ${e instanceof Error?e.message:String(e)}`)}const D=await eo();if(D){O||(console.error("[upgrade] Crash-loop rollback blocked: updater bootstrap unavailable"),process.exit(1));try{ro({fromVersion:z,targetVersion:D.from,oldDaemonPid:process.pid,operation:"rollback",rollbackJobId:D.jobId},{readiness:O}),console.error(`[upgrade] New version crashed ${D.attemptCount} times; external rollback scheduled.`),process.stdin.resume();return}catch(e){console.error(`[upgrade] Crash-loop rollback could not start: ${e instanceof Error?e.message:String(e)}`),process.exit(1)}}try{jo()}catch(e){console.error(`[personal-sync] data epoch initialization failed: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1;return}const m=p(),u=a.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??m.serverUrl??W.cn.url,h=de(),l=await io({serverUrl:u,authToken:m.machineToken??m.accessToken}),w=l.map(e=>e.type),V=N(l),f=new Co,U=new Ro,C=new No(f),I=new bo(u),ve=new xo({actionClient:I}),Ie=new $o({actionClient:I});let S=null,r;const q=Oo({authority:C,actionClient:I,attachmentCache:ve,attachmentUploader:Ie,observations:U,getMachineCredentials:()=>({machineToken:p().machineToken}),managerExecutor:e=>r.executeManagedManagerAction(e),scheduleExecutor:(e,n,o)=>oe({serverUrl:u,machineToken:p().machineToken,sessionId:e,action:n,args:o}),onPaused:({registryKey:e,sessionId:n})=>{const t=f.getByRegistryKey(e)?.binding;t&&f.setBinding({registryKey:e,bindingId:t.bindingId,roomId:t.roomId,agentIdentityId:t.agentIdentityId,agentMembershipId:t.agentMembershipId,status:"paused",activationMode:"paused"}),C.revokeSession(e),r?.discardPendingManagedRoomMessages(n)}});let L=null,j=null,H=null;const b=await me({paths:fo(),handlers:{...vo({daemonVersion:h,protocolVersion:wo,getStatus:()=>ie(),getAgents:()=>l,completeAgentAuthentication:e=>r.resumePendingAcpAuthentication(e),startUpgrade:async e=>{if(!L)throw new Error("upgrade_unavailable");return L(e)},prepareExternalUpgrade:e=>{if(!j)throw new Error("external_upgrade_unavailable");return j(e)},cancelExternalUpgrade:()=>{if(!H)throw new Error("external_upgrade_unavailable");return H()}}),...Io({getStatus:()=>ie(),getSnapshot:()=>N(l).map(e=>{const n=l.find(o=>o.type===e.type);return{...e,runtimePlatform:n?.runtimePlatform,runtimeIsWsl:n?.runtimeIsWsl}}),discover:async e=>{if(!r)throw new Error("daemon_starting");const n=await So(!0,e);if(e){const t=l.filter(A=>A.type!==e);l.splice(0,l.length,...t,...n.detectedAgents)}else l.splice(0,l.length,...n.detectedAgents);const o=N(l);return d.sendEvent({type:"event",event:"machine.agents",payload:{agentList:l.map(t=>t.type),agents:o}}),n.agents.map(t=>{const A=n.detectedAgents.find(B=>B.type===t.type);return{...t,runtimePlatform:A?.runtimePlatform,runtimeIsWsl:A?.runtimeIsWsl}})},validate:async e=>{if(!r)throw new Error("daemon_starting");return yo(e,{ignoreNativeSession:(n,o)=>R?.ignoreNativeSource(n,o)})}})}});let k;try{k=await me({paths:ho(),handlers:{"managed-room.execute":e=>q(e),"schedule.personal":async e=>{const n=e,o=ye(n?.context);if(f.getByNianSessionId(o)?.binding)throw new Error("schedule_personal_context_required");return await oe({serverUrl:u,machineToken:p().machineToken,sessionId:o,action:n.operation,args:n})}}})}catch(e){throw await b.close(),e}let Y=!1;const M=()=>{Y||(Y=!0,Promise.allSettled([b.close(),k.close()]).finally(()=>process.exit(0)))};if(process.once("SIGINT",M),process.once("SIGTERM",M),!m.machineToken){console.error('Shennian CLI is not paired. Run "shennian pair --browser" or "shennian pair --token" first.'),await Promise.allSettled([b.close(),k.close()]),process.exitCode=1;return}process.removeListener("SIGINT",M),process.removeListener("SIGTERM",M);const Q=`${zo(u)}/relay/machine`,_=m.machineToken;if(!_)return;try{await Lo({serverUrl:u,machineToken:_,machineId:m.machineId})}catch(e){console.error(`[personal-sync] machine authority verification failed: ${e instanceof Error?e.message:String(e)}`),await Promise.allSettled([b.close(),k.close()]),process.exit(1)}m.machineId&&lo(u,m.machineId),console.log(`[${new Date().toISOString()}] Connecting to ${Q}... (v${h}) agents: ${w.join(",")}`),T({level:"info",wsEvent:"daemon.start",metadata:{version:h,agents:w}});let R=null,P=null,x=0,v=oo();const Z=Ae(m.autoUpgrade),d=new Ee({serverUrl:Q,machineToken:_,cliVersion:h,agentList:w,autoUpgradePolicy:Z,sessionSyncProtocolVersion:pe,sessionSyncCapabilities:ge,daemonInstallationId:Vo(),dataEpoch:ue,updaterProtocolVersion:1,onConnected:()=>{console.log(`[${new Date().toISOString()}] \u2713 Connected`),T({level:"info",wsEvent:"daemon.connected"}),V.some(o=>o.models.length>0)&&d.sendEvent({type:"event",event:"machine.agents",payload:{agentList:w,agents:V}}),co(l,{serverUrl:m.serverUrl??u,authToken:m.machineToken??m.accessToken}).then(o=>{JSON.stringify(o)!==JSON.stringify(V)&&d.sendEvent({type:"event",event:"machine.agents",payload:{agentList:w,agents:o}})}).catch(()=>{}),v&&d.sendEvent({type:"event",event:"upgrade",payload:{state:"recovering",machineId:"self",from:v.from,to:v.to,...v.jobId?{jobId:v.jobId}:{},activeRunCount:r.getPendingRecoveryCount()}}),$.start(),P?.start();const e=++x,n=async()=>{try{if(await r.synchronizeSessionDeletions(),await r.recoverInterruptedRuns(),v){const o=v;v=null,o.jobId&&ao(o.jobId,o.to),Xe(),console.log(`[${new Date().toISOString()}] [upgrade] ${o.operation==="rollback"?"Rolled back":"Healthy"}: ${o.from} \u2192 ${o.to}`),mo({kind:"upgrade",result:o.operation==="rollback"?"rolled_back":"healthy",stage:"terminal"}),d.sendEvent({type:"event",event:"upgrade",payload:{state:o.operation==="rollback"?"rolled_back":"healthy",machineId:"self",from:o.from,to:o.to,...o.jobId?{jobId:o.jobId}:{}}}),o.operation!=="rollback"&&d.sendEvent({type:"event",event:"upgrade",payload:{state:"success",machineId:"self",from:o.from,to:o.to}})}e===x&&d.getState()==="connected"&&R?.handleConnected()}catch(o){if(console.error("[session.deletions] startup catch-up failed",o),e===x&&d.getState()==="connected"){if(o instanceof Bo)return;const t=o instanceof Ho?Math.max(1e3,o.retryAfterMs):5e3;setTimeout(()=>{n()},t).unref?.()}}};n()},onDisconnected:e=>{x+=1,r?.revokeProjectFsHandles(),P?.stop(),console.log(`[${new Date().toISOString()}] \u26A0 Disconnected, reconnecting... ${Jo(e)}`),T({level:"warn",wsEvent:"daemon.disconnected",metadata:{code:e.code,reason:e.reason,error:e.error,phase:e.phase,trigger:e.trigger,reconnectAttempt:e.reconnectAttempt}})},onProjectFsCapabilityChanged:e=>{e||r?.revokeProjectFsHandles()},onReq:e=>{if(console.log(`[${new Date().toISOString()}] [req] ${e.method}`),T({level:"info",type:"ws",wsEvent:e.method,wsDirection:"in",traceId:e.traceId}),e.method==="room.message.deliver"){Ao(d,P,e);return}r.handleReq(e)}});R=po()?new go(d):null,P=new Po({registry:f,observations:U,actionClient:I,getMachineCredentials:()=>({machineToken:p().machineToken}),isSessionDeleted:e=>new Eo().hasSession(e),onMessages:async(e,n,o,t)=>{await S?.processMessages(e,n,o,t)},onBindingPaused:e=>{C.revokeSession(e.registryKey),e.lifecycleState==="closed"?r?.retireManagedRoomSession(e.nianSessionId).catch(()=>{console.warn("[room.sync] retired session cleanup failed")}):r?.discardPendingManagedRoomMessages(e.nianSessionId)}});const $=new no({currentVersion:h,machineId:m.machineId??"unknown-machine",serverUrl:m.serverUrl??u,getIdleState:()=>r.getUpgradeIdleState(),sessionSyncProtocolVersion:pe,dataEpoch:ue,advertisedCapabilities:ge,updaterProtocolVersion:1,startUpgrade:async(e,n)=>{const{handleUpgradeStart:o}=await import("./commands/upgrade.js");await o(d,"auto-upgrade",e,{currentVersion:h,prepareForUpgrade:t=>r.prepareForUpgrade(t),cancelUpgradePreparation:()=>r.cancelUpgradePreparation(),shutdownAfterHandoff:()=>{setTimeout(()=>process.kill(process.pid,"SIGTERM"),100).unref?.()},suppressResponse:!0,waitForCompletion:!0,canHandoff:n?void 0:()=>r.getUpgradeIdleState().idle})}});$.setPolicy(Z),r=new qe(d,R,h,$,new ko({registry:f,observations:U,bindingClient:I,getMachineCredentials:()=>{const e=p();return{machineId:e.machineId,machineToken:e.machineToken}},hostProcessIdentity:()=>`daemon:${c}`}),{prepare:()=>Ne(),isDisabled:()=>ae(),shutdownAfterResponse:()=>{setTimeout(()=>{Oe(),process.kill(process.pid,"SIGTERM")},100).unref?.()}},e=>C.revokeToken(e),q,null,Ye(_),()=>N(l)),L=async e=>{const n=e&&typeof e=="object"&&!Array.isArray(e)?e:{},o=typeof n.targetVersion=="string"?n.targetVersion:void 0,t=n.confirmedMajor===!0;return await Ge(d,`local-upgrade-${Date.now()}`,o,{currentVersion:h,confirmedMajor:t,prepareForUpgrade:B=>r.prepareForUpgrade(B),cancelUpgradePreparation:()=>r.cancelUpgradePreparation(),shutdownAfterHandoff:()=>{setTimeout(()=>process.kill(process.pid,"SIGTERM"),100).unref?.()},suppressResponse:!0})},j=e=>{const n=e&&typeof e=="object"&&!Array.isArray(e)?e:{},t=`desktop-${(typeof n.targetVersion=="string"?n.targetVersion:"unknown").replace(/[^A-Za-z0-9._-]/g,"_")}-${Date.now()}`;return{jobId:t,...r.prepareForUpgrade(t)}},H=()=>(r.cancelUpgradePreparation(),{cancelled:!0}),S=new Uo({registry:f,observations:U,enqueueConversationBatch:e=>r.enqueueManagedRoomBatch(e),loadContext:async e=>{const n=p().machineToken?.trim(),o=e.binding;if(!n||!o)throw new Error("managed_room_context_unavailable");const t=await I.status({machineToken:n,roomId:o.roomId,bindingId:o.bindingId});if(!Do(f,e)||!To(e,t))throw new Error("managed_room_binding_unavailable");return{roomName:t.room.name,memberCount:t.room.memberCount,members:t.room.members,agentsByIdentityId:t.agent?{[t.agentIdentityId]:t.agent}:{},agentSystemPromptsByIdentityId:t.agent?{[t.agentIdentityId]:t.agent.systemPrompt??""}:{}}},issueManagedRoomContext:(e,n,o)=>{const t=p().machineToken;if(!t)throw new Error("machine_not_authorized");return C.issue({registryKey:e,machineCredential:t,activationCauseMessageId:n,allowedActions:o})},audit:new _o,resolveManagedAgentPolicy:e=>Mo({...e,runtime:l.find(n=>n.type===e.registration.runtimeAdapter)})}),r.scheduleHandler=e=>we({serverUrl:u,machineToken:p().machineToken,req:e,client:d,queue:r.getScheduleQueue(),registry:f}),r.getScheduleQueue().validateScheduledTarget=async e=>{if(!e.bindingId||!e.roomId)return"ready";if(f.getByNianSessionId(e.sessionId)?.binding?.bindingId!==e.bindingId)return"ended";const o=await I.status({machineToken:p().machineToken,roomId:e.roomId,bindingId:e.bindingId});return o.status==="active"&&o.room.status==="active"?"ready":"ended"},r.setManagedRoomTurnPreparer(e=>S.prepareConversationTurn(e)),r.setManagedAgentDirectTurnPreparer((e,n)=>S.prepareDirectConversationTurn(e,n)),r.setManagedRecoveryPreparer(async e=>e.source==="managed_direct"?S.prepareDirectConversationTurn(e.sessionId,{allowedRoomActions:[]}):e.source==="manager_event"?S.prepareDirectConversationTurn(e.sessionId,{allowedRoomActions:[],managerEvent:!0}):e.source!=="managed_room"||!e.roomContextRegistryKey||!e.activationCauseMessageId||!e.rootCauseMessageId||!e.activationSequence||e.activationHopCount===null?null:S.prepareConversationTurn([{registryKey:e.roomContextRegistryKey,causeMessageId:e.activationCauseMessageId,rootCauseMessageId:e.rootCauseMessageId,sequence:e.activationSequence,hopCount:e.activationHopCount}])),F.mkdirSync(fe,{recursive:!0}),se(process.pid,c,{version:h}),ce(process.pid,void 0,c),d.connect(),process.stdin.resume();let X=!1;const ee=async()=>{if(!X){X=!0,console.log(E.gray(`
|
|
3
|
+
Disconnecting...`)),T({level:"info",wsEvent:"daemon.stop"}),clearInterval(g);try{await r.cleanup(),$.stop(),R?.stop(),P?.stop(),d.disconnect(),await Promise.allSettled([b.close(),k.close()]),te(process.pid,c),re(c)}finally{process.exit(0)}}};process.on("SIGINT",()=>{ee()}),process.on("SIGTERM",()=>{ee()})}),i.command("workbuddy-session-hook",{hidden:!0}).description("(retired) WorkBuddy Room Hook").allowUnknownOption(!0).action(()=>J()),i.command("room-session-hook",{hidden:!0}).description("(internal) Authorize the official Room tool for one trusted Agent Session").argument("<runtime>","Trusted runtime adapter").allowUnknownOption(!0).action(()=>J()),i.command("room-tool-server",{hidden:!0}).description("(internal) Run the official Shennian Room MCP tool").allowUnknownOption(!0).action(()=>J());const he=i.command("config").description("View or modify configuration");he.command("show",{isDefault:!0}).description("Show current configuration").action(()=>{console.log(`Config file: ${Re()}`);const a=p(),s=De(a,ne(process.env,a));if(a.serverUrl){const g=Ze(a.serverUrl);console.log(`Region: ${g} (${W[g].label})`)}console.log(JSON.stringify(s,null,2))}),he.command("set").description("Update a config value").argument("<key>",'Config key (e.g. "server")').argument("<value>",'Config value (e.g. "cn" or "global")').action((a,s)=>{const g=p();if(a==="server"){s!=="cn"&&s!=="global"&&(console.error(E.red('\u2717 Value must be "cn" or "global"')),process.exit(1));const c=s;g.serverUrl=Qe(c),ke(g),console.log(E.green(`\u2713 Server set to ${W[c].label}`)),console.log(E.yellow(" Restart the background service for this to take effect: shennian stop && shennian start"));return}console.error(E.red(`\u2717 Unknown config key: ${a}. Supported: server`)),process.exit(1)}),Ue(i),_e(i),Le(i),je(i),He(i),Be(i),Fe(i),We(i),ze(i),i.parse();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type fs from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Physical transcript identity for cursor reuse. `st_dev` is excluded: macOS
|
|
4
|
+
* can renumber the same APFS volume across reboots, which would otherwise
|
|
5
|
+
* reset every cursor and replay all live transcripts from offset 0.
|
|
6
|
+
*/
|
|
7
|
+
export declare function nativeFileIdentity(stat: Pick<fs.Stats, 'ino' | 'birthtimeMs'>): string;
|
|
8
|
+
/** Legacy cursors persisted `dev:ino:birthtime`; compare them without the device. */
|
|
9
|
+
export declare function normalizeNativeFileIdentity(identity: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function i(t){return`${String(t.ino)}:${String(t.birthtimeMs)}`}function e(t){const n=t.split(":");return n.length===3?`${n[1]}:${n[2]}`:t}export{i as nativeFileIdentity,e as normalizeNativeFileIdentity};
|
|
@@ -67,6 +67,12 @@ export declare class NativeFusionLocalRepository {
|
|
|
67
67
|
completeManagedClaim(canonicalMessageId: string, suppression?: ManagedSuppressionWindow): void;
|
|
68
68
|
removeManagedSuppression(agentType: string, sourceSessionKey: string): void;
|
|
69
69
|
listSourceBindings(): SourceBinding[];
|
|
70
|
+
/** Raw source → session bindings, including managed-echo suppressions and deleted sources. */
|
|
71
|
+
listSourceOwners(): Array<{
|
|
72
|
+
agentType: string;
|
|
73
|
+
sourceSessionKey: string;
|
|
74
|
+
sessionId: string;
|
|
75
|
+
}>;
|
|
70
76
|
purgeSession(sessionId: string): void;
|
|
71
77
|
isSourceInternal(agentType: ParsedNativeEvent['agentType'], sourceSessionKey: string): boolean;
|
|
72
78
|
isSourceDeleted(agentType: ParsedNativeEvent['agentType'], sourceSessionKey: string): boolean;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{isToolPayload as B,sessionPreviewFromPayload as L}from"@shennian/wire";import y from"node:crypto";import d from"node:fs";import u from"node:path";import{recordPersonalSyncLocalMetric as C}from"../personal-sync/metrics.js";import{resolvePersonalSyncV1Path as P}from"../personal-sync/paths.js";import{getCanonicalSessionStore as H,getDaemonInstallationId as U,listSessionRecords as K,recordSession as w}from"../session/store.js";import{readToolDetail as V,writeToolDetail as q}from"../session/tool-detail-store.js";import{NativeSourceDeletionStore as J}from"./native-source-deletions.js";import{isInternalNativeSource as W}from"./source-visibility.js";const E=1;class z extends Error{code="missing_user_boundary";constructor(e){super(`native source ${e} does not start at a visible user boundary`),this.name="MissingUserBoundaryError"}}class G extends Error{code="internal_native_source";constructor(){super("internal native source cannot be imported as a personal session"),this.name="InternalNativeSourceError"}}class Q extends Error{code="native_source_deleted";constructor(e){super(`native source ${e} is blocked by a durable deletion tombstone`),this.name="NativeSourceDeletedError"}}class X extends Error{sourceEventKey;code="native_event_identity_conflict";constructor(e){super(`native event identity conflict: ${e}`),this.sourceEventKey=e,this.name="NativeEventIdentityConflictError"}}class pe{options;constructor(e={}){this.options=e}commitEvents(e){return[...e].filter(s=>!this.isSourceInternal(s.agentType,s.sourceSessionKey)).sort((s,o)=>s.ts-o.ts).map(s=>this.commitEvent(s))}updateMessageDeliveryState(e,s,o){if(!this.canonicalStore().updateMessageDeliveryState(e,s,o))return;const i=(this.options.listLocalSessions??K)().find(a=>a.sessionId===e);if(!i)return;(this.options.recordLocalSession??w)({...i})}commitEvent(e){if(Y(e),this.isSourceInternal(e.agentType,e.sourceSessionKey))throw new G;if(this.isSourceDeleted(e.agentType,e.sourceSessionKey))throw new Q(e.sourceSessionKey);const s=this.installationId(),o=this.readSources(),n=b(e.agentType,e.sourceSessionKey),i=o.sources[n],c=se(e),a=this.ledgerFile(e.agentType,e.sourceEventKey),r=d.existsSync(a)?D(a):null;if(r&&r.daemonInstallationId!==s)throw new Error("native event ledger belongs to another daemon installation");if(r&&r.eventFingerprint!==c)throw new X(e.sourceEventKey);if(!i&&!r&&e.role!=="user")throw new z(e.sourceSessionKey);const g=e.claimSessionId??null,l=i?.sessionId??r?.sessionId??g??Z(s,e.agentType,e.sourceSessionKey);if(g&&i&&i.sessionId!==g)throw new Error(`managed claim session conflicts with source binding ${e.sourceSessionKey}`);const f=this.canonicalStore(),I=e.localDisposition==="managed_echo_suppressed"||r?.mappedBy==="managed_echo_suppressed",m=r?.canonicalMessageId??(I?null:e.claimCanonicalMessageId??ee(s,e.agentType,e.canonicalMessageKey??e.sourceEventKey));let h=f.getManifest(l)?.bodyRevision??0,T=!!r;const M=!I&&!!e.claimCanonicalMessageId;if(M){if(!m)throw new Error("managed echo claim requires a canonical message");const S=f.getMessageState(l,m);if(!S||S.tombstoned)throw new Error(`managed echo claim target is not durable: ${m}`);S.deliveryState==="queued"&&S.message&&(h=f.upsert(l,{...S.message,ts:e.ts},"executing").bodyRevision)}if(!r&&!M&&!I){if(!m)throw new Error("native event requires a canonical message");const S=e.canonicalMessageKey?f.getMessageState(l,m):null,F=e.canonicalMutation==="upsert"||!!S,_=F?S??f.getMessageState(l,m):null,v={id:m,sessionId:l,role:e.role,ts:_?.message?.ts??e.ts,payload:te(_?.message?.payload,e.payload)},$=F?f.upsert(l,v):f.append(l,v);if(h=$.bodyRevision,T=$.duplicate,e.localToolDetail){const k=this.options.writeLocalToolDetail??q,j=this.options.readLocalToolDetail??V,R=oe(j(l,e.localToolDetail.toolCallId),e.localToolDetail);k({sessionId:l,...R,ts:e.ts,detailRef:{toolIndex:0}})}}e.terminal&&(h=f.completeStartedUserMessages(l,e.ts).at(-1)?.bodyRevision??h),(!r||e.terminal)&&!I&&this.recordIndex(e,l,m,h);const x=new Date().toISOString();return r||p(a,{schemaVersion:E,daemonInstallationId:s,sessionId:l,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceEventKey:e.sourceEventKey,eventFingerprint:c,canonicalMessageId:m,canonicalRevision:h,cursor:e.cursor??null,mappedBy:I?"managed_echo_suppressed":M?"managed_echo_claim":e.sourceMode,eventTs:e.ts,createdAt:x}),o.sources[n]={schemaVersion:E,daemonInstallationId:s,sessionId:l,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceMode:r?i?.sourceMode??e.sourceMode:e.sourceMode,lastCursor:r?i?.lastCursor??r.cursor:e.cursor??i?.lastCursor??null,bodyRevision:h,createdAt:i?.createdAt??x,updatedAt:x},p(this.sourcesFile(),o),!r&&e.cursor&&C("sourceCursorAdvanced"),!r&&!M&&!I&&!T&&C("canonicalCommits"),{sessionId:l,messageId:m,bodyRevision:h,duplicate:T,claimed:M,suppressed:I}}repairMissingUserBoundaries(){const e=this.options.listLocalSessions??K,s=this.options.recordLocalSession??w,o=this.canonicalStore();let n=0;for(const i of e()){if(i.localReady)continue;const a=o.getManifest(i.sessionId)?.firstAvailableSeq;if(a==null)continue;const r=o.readPage(i.sessionId,{limit:1,beforeSequence:a+1}).entries[0];!r||r.sequence!==a||r.message.role!=="user"||(s({sessionId:i.sessionId,agentType:i.agentType,sessionMode:i.sessionMode,managerConfig:i.managerConfig,workDir:i.workDir,agentSessionId:i.agentSessionId,modelId:i.modelId,managerDefaultWorkerAgentType:i.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:i.managerDefaultWorkerModelId,status:i.status,lastActivityAt:i.lastActivityAt,lastMessagePreview:i.lastMessagePreview,localReady:!0,firstUserMessageId:r.message.id,firstUserBodyRevision:r.revision}),n+=1)}return n}listManagedClaims(){return Object.values(this.readManagedEchoState().claims)}listManagedSuppressions(){return Object.values(this.readManagedEchoState().suppressions)}upsertManagedClaim(e){A(e);const s=this.readManagedEchoState();s.claims[e.canonicalMessageId]=e,p(this.managedEchoStateFile(),s)}updateManagedClaimSource(e,s,o){const n=this.readManagedEchoState();let i=!1;for(const c of Object.values(n.claims))c.sessionId!==e||c.sourceAgentType!==s||(c.sourceSessionKey=o,i=!0);i&&p(this.managedEchoStateFile(),n)}removeManagedClaim(e){const s=this.readManagedEchoState();s.claims[e]&&(delete s.claims[e],p(this.managedEchoStateFile(),s))}completeManagedClaim(e,s){const o=this.readManagedEchoState();delete o.claims[e],s&&(N(s),o.suppressions[b(s.agentType,s.sourceSessionKey)]=s),p(this.managedEchoStateFile(),o)}removeManagedSuppression(e,s){const o=this.readManagedEchoState(),n=b(e,s);o.suppressions[n]&&(delete o.suppressions[n],p(this.managedEchoStateFile(),o))}listSourceBindings(){return Object.values(this.readSources().sources).filter(e=>!this.isSourceDeleted(e.agentType,e.sourceSessionKey))}purgeSession(e){if(!e)throw new Error("sessionId is required");const s=this.readSources();let o=!1;for(const[a,r]of Object.entries(s.sources))r.sessionId===e&&(delete s.sources[a],o=!0);o&&p(this.sourcesFile(),s);const n=this.readManagedEchoState();let i=!1;for(const[a,r]of Object.entries(n.claims))r.sessionId===e&&(delete n.claims[a],i=!0);for(const[a,r]of Object.entries(n.suppressions))r.sessionId===e&&(delete n.suppressions[a],i=!0);i&&p(this.managedEchoStateFile(),n);const c=u.join(this.rootDir(),"ledger");if(d.existsSync(c))for(const a of d.readdirSync(c,{withFileTypes:!0})){if(!a.isFile()||!a.name.endsWith(".json"))continue;const r=u.join(c,a.name),g=D(r);if(g.daemonInstallationId!==this.installationId())throw new Error("native event ledger belongs to another daemon installation");g.sessionId===e&&d.unlinkSync(r)}}isSourceInternal(e,s){const o=this.rootDir();return W(e,s,{filePath:u.join(u.basename(o)==="native-fusion"?u.dirname(o):o,"native-fusion-state.json"),daemonInstallationId:this.installationId()})}isSourceDeleted(e,s){return(this.options.deletionStore??new J({rootDir:u.basename(this.rootDir())==="native-fusion"?u.dirname(this.rootDir()):this.rootDir(),daemonInstallationId:this.installationId()})).has(e,s)}observeNativeTitle(e,s,o){if(this.isSourceInternal(e,s))return!1;const n=o.trim().slice(0,120);if(!n)return!1;const i=this.readSources().sources[b(e,s)];if(!i||this.isSourceDeleted(e,s))return!1;const c=(this.options.listLocalSessions??K)().find(g=>g.sessionId===i.sessionId);if(c?.titleLockedByUser||c?.titleSource==="agent_native"&&c.title===n)return!1;const a=y.createHash("sha256").update(`${e}\0${s}\0${n}`).digest("hex");return(this.options.recordLocalSession??w)({sessionId:i.sessionId,agentType:i.agentType,workDir:c?.workDir??"",agentSessionId:s,title:n,titleSource:"agent_native",titleOriginEventId:`agent-native:${a}`}),!0}recordIndex(e,s,o,n){const i=this.options.recordLocalSession??w,c=e.terminal?`terminal:${y.createHash("sha256").update(e.sourceEventKey).digest("hex")}`:void 0,a=e.role==="user"||e.role==="agent"?O(e.payload):null,r=a&&o?{lastMessagePreview:a,lastMessagePreviewMessageId:o,lastMessagePreviewAt:new Date(e.ts).toISOString()}:{},g=o&&(a||B(e.payload))?new Date(e.ts).toISOString():void 0;i({sessionId:s,agentType:e.agentType,workDir:e.workDir??"",agentSessionId:e.sourceSessionKey,modelId:e.modelId??null,status:e.terminal?e.terminalState==="failed"||e.terminalState==="aborted"?"failed":"completed":"active",attentionId:c,lastActivityAt:new Date(e.ts).toISOString(),lastVisibleEventAt:g,...r,...e.role==="user"&&o?{localReady:!0,firstUserMessageId:o,firstUserBodyRevision:n}:{},title:e.title?.trim()||(e.role==="user"?O(e.payload)??"":void 0),titleSource:e.title?.trim()?e.titleSource??"first_user_fallback":"first_user_fallback",titleOriginEventId:e.sourceEventKey})}readSources(){const e=this.sourcesFile();if(!d.existsSync(e))return{schemaVersion:E,daemonInstallationId:this.installationId(),sources:{}};const s=D(e);if(s.schemaVersion!==E||s.daemonInstallationId!==this.installationId()||!s.sources)throw new Error("invalid native source binding store");for(const o of Object.values(s.sources))if(o.daemonInstallationId!==this.installationId())throw new Error("native source binding belongs to another daemon installation");return s}installationId(){return this.options.daemonInstallationId??U()}canonicalStore(){return this.options.canonicalStore??H()}rootDir(){return this.options.rootDir??P("native-fusion")}sourcesFile(){return u.join(this.rootDir(),"sources.json")}managedEchoStateFile(){return u.join(this.rootDir(),"managed-echo-state.json")}readManagedEchoState(){const e=this.managedEchoStateFile();if(!d.existsSync(e))return{schemaVersion:E,daemonInstallationId:this.installationId(),claims:{},suppressions:{}};const s=D(e);if(s.schemaVersion!==E||s.daemonInstallationId!==this.installationId()||!s.claims||!s.suppressions)throw new Error("invalid managed echo state");for(const o of Object.values(s.claims))A(o);for(const o of Object.values(s.suppressions))N(o);return s}ledgerFile(e,s){const o=y.createHash("sha256").update(`${e}\0${s}`).digest("hex");return u.join(this.rootDir(),"ledger",`${o}.json`)}}function Y(t){if(!t.agentType||!t.sourceSessionKey||!t.sourceEventKey||!t.payload)throw new Error("invalid native event");if(!Number.isFinite(t.ts))throw new Error("invalid native event timestamp")}function b(t,e){return y.createHash("sha256").update(`${t}\0${e}`).digest("hex")}function Z(t,e,s){return`native-session-${y.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function ee(t,e,s){return`native-${y.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function se(t){return y.createHash("sha256").update(JSON.stringify({agentType:t.agentType,sourceSessionKey:t.sourceSessionKey,sourceEventKey:t.sourceEventKey,role:t.role,ts:t.ts,payload:t.payload,canonicalMessageKey:t.canonicalMessageKey??null,canonicalMutation:t.canonicalMutation??"append"})).digest("hex")}function A(t){if(!t.sessionId||!t.canonicalMessageId||!t.textHash||!Number.isFinite(t.createdAt)||!["codex","claude","opencode"].includes(t.sourceAgentType)||!["suppress_following","import_following"].includes(t.managedEchoPolicy))throw new Error("invalid managed echo claim")}function N(t){if(!t.sessionId||!t.sourceSessionKey||!t.claimSourceEventKey||!["codex","claude","opencode"].includes(t.agentType)||!Number.isFinite(t.startedAt)||!Number.isFinite(t.endsAt)||t.endsAt<t.startedAt)throw new Error("invalid managed echo suppression")}function te(t,e){if(!t)return e;try{const s=JSON.parse(t),o=JSON.parse(e);if(s.type!=="tool"||o.type!=="tool"||s.toolCallId!==o.toolCallId)return e;const n=typeof o.name=="string"?o.name:"",i=/(?:_output|_result)$/.test(n);return JSON.stringify({...s,...o,...i&&typeof s.name=="string"?{name:s.name}:{}})}catch{return e}}function oe(t,e){if(!t||t.toolCallId!==e.toolCallId)return e;const s=/(?:_output|_result)$/.test(e.name);return{...e,name:s?t.name:e.name,...e.args===void 0&&t.args!==void 0?{args:t.args}:{},...e.result===void 0&&t.result!==void 0?{result:t.result}:{}}}function O(t){return L(t,120)}function p(t,e){const s=u.dirname(t);d.mkdirSync(s,{recursive:!0,mode:448});const o=`${t}.tmp-${process.pid}-${y.randomBytes(6).toString("hex")}`;let n;try{n=d.openSync(o,"wx",384),d.writeFileSync(n,`${JSON.stringify(e,null,2)}
|
|
1
|
+
import{isToolPayload as B,sessionPreviewFromPayload as L}from"@shennian/wire";import y from"node:crypto";import d from"node:fs";import u from"node:path";import{recordPersonalSyncLocalMetric as C}from"../personal-sync/metrics.js";import{resolvePersonalSyncV1Path as P}from"../personal-sync/paths.js";import{getCanonicalSessionStore as H,getDaemonInstallationId as U,listSessionRecords as K,recordSession as w}from"../session/store.js";import{readToolDetail as V,writeToolDetail as q}from"../session/tool-detail-store.js";import{NativeSourceDeletionStore as J}from"./native-source-deletions.js";import{isInternalNativeSource as W}from"./source-visibility.js";const E=1;class z extends Error{code="missing_user_boundary";constructor(e){super(`native source ${e} does not start at a visible user boundary`),this.name="MissingUserBoundaryError"}}class G extends Error{code="internal_native_source";constructor(){super("internal native source cannot be imported as a personal session"),this.name="InternalNativeSourceError"}}class Q extends Error{code="native_source_deleted";constructor(e){super(`native source ${e} is blocked by a durable deletion tombstone`),this.name="NativeSourceDeletedError"}}class X extends Error{sourceEventKey;code="native_event_identity_conflict";constructor(e){super(`native event identity conflict: ${e}`),this.sourceEventKey=e,this.name="NativeEventIdentityConflictError"}}class pe{options;constructor(e={}){this.options=e}commitEvents(e){return[...e].filter(s=>!this.isSourceInternal(s.agentType,s.sourceSessionKey)).sort((s,o)=>s.ts-o.ts).map(s=>this.commitEvent(s))}updateMessageDeliveryState(e,s,o){if(!this.canonicalStore().updateMessageDeliveryState(e,s,o))return;const i=(this.options.listLocalSessions??K)().find(a=>a.sessionId===e);if(!i)return;(this.options.recordLocalSession??w)({...i})}commitEvent(e){if(Y(e),this.isSourceInternal(e.agentType,e.sourceSessionKey))throw new G;if(this.isSourceDeleted(e.agentType,e.sourceSessionKey))throw new Q(e.sourceSessionKey);const s=this.installationId(),o=this.readSources(),n=b(e.agentType,e.sourceSessionKey),i=o.sources[n],c=se(e),a=this.ledgerFile(e.agentType,e.sourceEventKey),r=d.existsSync(a)?D(a):null;if(r&&r.daemonInstallationId!==s)throw new Error("native event ledger belongs to another daemon installation");if(r&&r.eventFingerprint!==c)throw new X(e.sourceEventKey);if(!i&&!r&&e.role!=="user")throw new z(e.sourceSessionKey);const g=e.claimSessionId??null,l=i?.sessionId??r?.sessionId??g??Z(s,e.agentType,e.sourceSessionKey);if(g&&i&&i.sessionId!==g)throw new Error(`managed claim session conflicts with source binding ${e.sourceSessionKey}`);const f=this.canonicalStore(),I=e.localDisposition==="managed_echo_suppressed"||r?.mappedBy==="managed_echo_suppressed",m=r?.canonicalMessageId??(I?null:e.claimCanonicalMessageId??ee(s,e.agentType,e.canonicalMessageKey??e.sourceEventKey));let h=f.getManifest(l)?.bodyRevision??0,T=!!r;const M=!I&&!!e.claimCanonicalMessageId;if(M){if(!m)throw new Error("managed echo claim requires a canonical message");const S=f.getMessageState(l,m);if(!S||S.tombstoned)throw new Error(`managed echo claim target is not durable: ${m}`);S.deliveryState==="queued"&&S.message&&(h=f.upsert(l,{...S.message,ts:e.ts},"executing").bodyRevision)}if(!r&&!M&&!I){if(!m)throw new Error("native event requires a canonical message");const S=e.canonicalMessageKey?f.getMessageState(l,m):null,v=e.canonicalMutation==="upsert"||!!S,F=v?S??f.getMessageState(l,m):null,_={id:m,sessionId:l,role:e.role,ts:F?.message?.ts??e.ts,payload:te(F?.message?.payload,e.payload)},$=v?f.upsert(l,_):f.append(l,_);if(h=$.bodyRevision,T=$.duplicate,e.localToolDetail){const j=this.options.writeLocalToolDetail??q,k=this.options.readLocalToolDetail??V,R=oe(k(l,e.localToolDetail.toolCallId),e.localToolDetail);j({sessionId:l,...R,ts:e.ts,detailRef:{toolIndex:0}})}}e.terminal&&(h=f.completeStartedUserMessages(l,e.ts).at(-1)?.bodyRevision??h),(!r||e.terminal)&&!I&&this.recordIndex(e,l,m,h);const x=new Date().toISOString();return r||p(a,{schemaVersion:E,daemonInstallationId:s,sessionId:l,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceEventKey:e.sourceEventKey,eventFingerprint:c,canonicalMessageId:m,canonicalRevision:h,cursor:e.cursor??null,mappedBy:I?"managed_echo_suppressed":M?"managed_echo_claim":e.sourceMode,eventTs:e.ts,createdAt:x}),o.sources[n]={schemaVersion:E,daemonInstallationId:s,sessionId:l,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceMode:r?i?.sourceMode??e.sourceMode:e.sourceMode,lastCursor:r?i?.lastCursor??r.cursor:e.cursor??i?.lastCursor??null,bodyRevision:h,createdAt:i?.createdAt??x,updatedAt:x},p(this.sourcesFile(),o),!r&&e.cursor&&C("sourceCursorAdvanced"),!r&&!M&&!I&&!T&&C("canonicalCommits"),{sessionId:l,messageId:m,bodyRevision:h,duplicate:T,claimed:M,suppressed:I}}repairMissingUserBoundaries(){const e=this.options.listLocalSessions??K,s=this.options.recordLocalSession??w,o=this.canonicalStore();let n=0;for(const i of e()){if(i.localReady)continue;const a=o.getManifest(i.sessionId)?.firstAvailableSeq;if(a==null)continue;const r=o.readPage(i.sessionId,{limit:1,beforeSequence:a+1}).entries[0];!r||r.sequence!==a||r.message.role!=="user"||(s({sessionId:i.sessionId,agentType:i.agentType,sessionMode:i.sessionMode,managerConfig:i.managerConfig,workDir:i.workDir,agentSessionId:i.agentSessionId,modelId:i.modelId,managerDefaultWorkerAgentType:i.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:i.managerDefaultWorkerModelId,status:i.status,lastActivityAt:i.lastActivityAt,lastMessagePreview:i.lastMessagePreview,localReady:!0,firstUserMessageId:r.message.id,firstUserBodyRevision:r.revision}),n+=1)}return n}listManagedClaims(){return Object.values(this.readManagedEchoState().claims)}listManagedSuppressions(){return Object.values(this.readManagedEchoState().suppressions)}upsertManagedClaim(e){O(e);const s=this.readManagedEchoState();s.claims[e.canonicalMessageId]=e,p(this.managedEchoStateFile(),s)}updateManagedClaimSource(e,s,o){const n=this.readManagedEchoState();let i=!1;for(const c of Object.values(n.claims))c.sessionId!==e||c.sourceAgentType!==s||(c.sourceSessionKey=o,i=!0);i&&p(this.managedEchoStateFile(),n)}removeManagedClaim(e){const s=this.readManagedEchoState();s.claims[e]&&(delete s.claims[e],p(this.managedEchoStateFile(),s))}completeManagedClaim(e,s){const o=this.readManagedEchoState();delete o.claims[e],s&&(A(s),o.suppressions[b(s.agentType,s.sourceSessionKey)]=s),p(this.managedEchoStateFile(),o)}removeManagedSuppression(e,s){const o=this.readManagedEchoState(),n=b(e,s);o.suppressions[n]&&(delete o.suppressions[n],p(this.managedEchoStateFile(),o))}listSourceBindings(){return Object.values(this.readSources().sources).filter(e=>!this.isSourceDeleted(e.agentType,e.sourceSessionKey))}listSourceOwners(){return Object.values(this.readSources().sources).map(({agentType:e,sourceSessionKey:s,sessionId:o})=>({agentType:e,sourceSessionKey:s,sessionId:o}))}purgeSession(e){if(!e)throw new Error("sessionId is required");const s=this.readSources();let o=!1;for(const[a,r]of Object.entries(s.sources))r.sessionId===e&&(delete s.sources[a],o=!0);o&&p(this.sourcesFile(),s);const n=this.readManagedEchoState();let i=!1;for(const[a,r]of Object.entries(n.claims))r.sessionId===e&&(delete n.claims[a],i=!0);for(const[a,r]of Object.entries(n.suppressions))r.sessionId===e&&(delete n.suppressions[a],i=!0);i&&p(this.managedEchoStateFile(),n);const c=u.join(this.rootDir(),"ledger");if(d.existsSync(c))for(const a of d.readdirSync(c,{withFileTypes:!0})){if(!a.isFile()||!a.name.endsWith(".json"))continue;const r=u.join(c,a.name),g=D(r);if(g.daemonInstallationId!==this.installationId())throw new Error("native event ledger belongs to another daemon installation");g.sessionId===e&&d.unlinkSync(r)}}isSourceInternal(e,s){const o=this.rootDir();return W(e,s,{filePath:u.join(u.basename(o)==="native-fusion"?u.dirname(o):o,"native-fusion-state.json"),daemonInstallationId:this.installationId()})}isSourceDeleted(e,s){return(this.options.deletionStore??new J({rootDir:u.basename(this.rootDir())==="native-fusion"?u.dirname(this.rootDir()):this.rootDir(),daemonInstallationId:this.installationId()})).has(e,s)}observeNativeTitle(e,s,o){if(this.isSourceInternal(e,s))return!1;const n=o.trim().slice(0,120);if(!n)return!1;const i=this.readSources().sources[b(e,s)];if(!i||this.isSourceDeleted(e,s))return!1;const c=(this.options.listLocalSessions??K)().find(g=>g.sessionId===i.sessionId);if(c?.titleLockedByUser||c?.titleSource==="agent_native"&&c.title===n)return!1;const a=y.createHash("sha256").update(`${e}\0${s}\0${n}`).digest("hex");return(this.options.recordLocalSession??w)({sessionId:i.sessionId,agentType:i.agentType,workDir:c?.workDir??"",agentSessionId:s,title:n,titleSource:"agent_native",titleOriginEventId:`agent-native:${a}`}),!0}recordIndex(e,s,o,n){const i=this.options.recordLocalSession??w,c=e.terminal?`terminal:${y.createHash("sha256").update(e.sourceEventKey).digest("hex")}`:void 0,a=e.role==="user"||e.role==="agent"?N(e.payload):null,r=a&&o?{lastMessagePreview:a,lastMessagePreviewMessageId:o,lastMessagePreviewAt:new Date(e.ts).toISOString()}:{},g=o&&(a||B(e.payload))?new Date(e.ts).toISOString():void 0;i({sessionId:s,agentType:e.agentType,workDir:e.workDir??"",agentSessionId:e.sourceSessionKey,modelId:e.modelId??null,status:e.terminal?e.terminalState==="failed"||e.terminalState==="aborted"?"failed":"completed":"active",attentionId:c,lastActivityAt:new Date(e.ts).toISOString(),lastVisibleEventAt:g,...r,...e.role==="user"&&o?{localReady:!0,firstUserMessageId:o,firstUserBodyRevision:n}:{},title:e.title?.trim()||(e.role==="user"?N(e.payload)??"":void 0),titleSource:e.title?.trim()?e.titleSource??"first_user_fallback":"first_user_fallback",titleOriginEventId:e.sourceEventKey})}readSources(){const e=this.sourcesFile();if(!d.existsSync(e))return{schemaVersion:E,daemonInstallationId:this.installationId(),sources:{}};const s=D(e);if(s.schemaVersion!==E||s.daemonInstallationId!==this.installationId()||!s.sources)throw new Error("invalid native source binding store");for(const o of Object.values(s.sources))if(o.daemonInstallationId!==this.installationId())throw new Error("native source binding belongs to another daemon installation");return s}installationId(){return this.options.daemonInstallationId??U()}canonicalStore(){return this.options.canonicalStore??H()}rootDir(){return this.options.rootDir??P("native-fusion")}sourcesFile(){return u.join(this.rootDir(),"sources.json")}managedEchoStateFile(){return u.join(this.rootDir(),"managed-echo-state.json")}readManagedEchoState(){const e=this.managedEchoStateFile();if(!d.existsSync(e))return{schemaVersion:E,daemonInstallationId:this.installationId(),claims:{},suppressions:{}};const s=D(e);if(s.schemaVersion!==E||s.daemonInstallationId!==this.installationId()||!s.claims||!s.suppressions)throw new Error("invalid managed echo state");for(const o of Object.values(s.claims))O(o);for(const o of Object.values(s.suppressions))A(o);return s}ledgerFile(e,s){const o=y.createHash("sha256").update(`${e}\0${s}`).digest("hex");return u.join(this.rootDir(),"ledger",`${o}.json`)}}function Y(t){if(!t.agentType||!t.sourceSessionKey||!t.sourceEventKey||!t.payload)throw new Error("invalid native event");if(!Number.isFinite(t.ts))throw new Error("invalid native event timestamp")}function b(t,e){return y.createHash("sha256").update(`${t}\0${e}`).digest("hex")}function Z(t,e,s){return`native-session-${y.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function ee(t,e,s){return`native-${y.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function se(t){return y.createHash("sha256").update(JSON.stringify({agentType:t.agentType,sourceSessionKey:t.sourceSessionKey,sourceEventKey:t.sourceEventKey,role:t.role,ts:t.ts,payload:t.payload,canonicalMessageKey:t.canonicalMessageKey??null,canonicalMutation:t.canonicalMutation??"append"})).digest("hex")}function O(t){if(!t.sessionId||!t.canonicalMessageId||!t.textHash||!Number.isFinite(t.createdAt)||!["codex","claude","opencode"].includes(t.sourceAgentType)||!["suppress_following","import_following"].includes(t.managedEchoPolicy))throw new Error("invalid managed echo claim")}function A(t){if(!t.sessionId||!t.sourceSessionKey||!t.claimSourceEventKey||!["codex","claude","opencode"].includes(t.agentType)||!Number.isFinite(t.startedAt)||!Number.isFinite(t.endsAt)||t.endsAt<t.startedAt)throw new Error("invalid managed echo suppression")}function te(t,e){if(!t)return e;try{const s=JSON.parse(t),o=JSON.parse(e);if(s.type!=="tool"||o.type!=="tool"||s.toolCallId!==o.toolCallId)return e;const n=typeof o.name=="string"?o.name:"",i=/(?:_output|_result)$/.test(n);return JSON.stringify({...s,...o,...i&&typeof s.name=="string"?{name:s.name}:{}})}catch{return e}}function oe(t,e){if(!t||t.toolCallId!==e.toolCallId)return e;const s=/(?:_output|_result)$/.test(e.name);return{...e,name:s?t.name:e.name,...e.args===void 0&&t.args!==void 0?{args:t.args}:{},...e.result===void 0&&t.result!==void 0?{result:t.result}:{}}}function N(t){return L(t,120)}function p(t,e){const s=u.dirname(t);d.mkdirSync(s,{recursive:!0,mode:448});const o=`${t}.tmp-${process.pid}-${y.randomBytes(6).toString("hex")}`;let n;try{n=d.openSync(o,"wx",384),d.writeFileSync(n,`${JSON.stringify(e,null,2)}
|
|
2
2
|
`,"utf8"),d.fsyncSync(n),d.closeSync(n),n=void 0,d.renameSync(o,t)}finally{n!==void 0&&d.closeSync(n);try{d.unlinkSync(o)}catch{}}}function D(t){return JSON.parse(d.readFileSync(t,"utf8"))}export{G as InternalNativeSourceError,z as MissingUserBoundaryError,X as NativeEventIdentityConflictError,pe as NativeFusionLocalRepository,Q as NativeSourceDeletedError};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
`).trim()}function F(){return new Promise(f=>setImmediate(f))}function fe(f){return f.split(/[\\/]+/).includes(".codex")}function k(f){try{return N.statSync(f).mtimeMs}catch{return 0}}class Me{client;repository;timer=null;activeTimer=null;scanPromise=null;queuedTargetFiles=new Set;pendingBoundaryFiles=new Set;sourceFiles=new Map;fullScanQueued=!1;watchEnabled=!1;lifecycleRevision=0;watcher=new V(e=>{this.scanNow(e).catch(s=>{console.error("[native-fusion] watched file scan failed",s)})});pendingClaims;suppressionWindows;externalObservations=new Map;externalTerminalHandler=null;externalClaimHandler=null;ignoredSources=new Set;constructor(e,s=new Y){this.client=e,this.repository=s;const n=C();for(const t of[...z(H()),...U(n.daemonInstallationId)])j(t.sessionId,t.agentType,t.sourceSessionKey);this.ignoredSources=new Set(Object.keys(C().ignoredSources??{})),this.pendingClaims=new Map((this.repository.listManagedClaims?.()??[]).map(t=>[t.canonicalMessageId,t])),this.suppressionWindows=new Map((this.repository.listManagedSuppressions?.()??[]).map(t=>[g(t.agentType,t.sourceSessionKey),t]))}ignoreNativeSource(e,s){if(!["codex","claude","opencode"].includes(e)||!s)return;const n=g(e,s);if(this.ignoredSources.has(n))return;this.ignoredSources.add(n);const t=C();for(const r of Object.keys(t.ignoredSources??{}))this.ignoredSources.add(r);t.ignoredSources=Object.fromEntries([...this.ignoredSources].sort().map(r=>[r,!0])),B(t)}start(){this.timer||(this.timer=setInterval(()=>{this.scanNow().catch(e=>{console.error("[native-fusion] periodic scan failed",e)})},oe))}stop(){this.lifecycleRevision+=1,this.timer&&clearInterval(this.timer),this.timer=null,this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null,this.pendingBoundaryFiles.clear(),this.watchEnabled=!1,this.watcher.stop()}setExternalTerminalHandler(e){this.externalTerminalHandler=e}setExternalClaimHandler(e){if(this.externalClaimHandler=e,!!e)for(const s of this.externalObservations.values())for(const n of s)n.state==="running"&&e(n.sessionId,n.canonicalMessageId)}restoreExternalObservation(e){if(!e.sessionId||!e.sourceSessionKey||!e.runId||!e.canonicalMessageId||!Number.isFinite(e.createdAt))return!1;const s=`codex:${e.sourceSessionKey}`,n=this.externalObservations.get(s)??[];if(n.some(u=>u.runId===e.runId))return!0;const t=Date.now(),r={...e,state:this.pendingClaims.has(e.canonicalMessageId)?"queued":"running",createdAt:Math.min(e.createdAt,t),lastPublishedAt:t};return n.push(r),n.sort((u,h)=>u.createdAt-h.createdAt),this.externalObservations.set(s,n),this.emitExternalActivity(r,r.state),r.state==="running"&&this.externalClaimHandler?.(r.sessionId,r.canonicalMessageId),this.ensureActiveScanning(!1),!0}handleConnected(){const e=++this.lifecycleRevision;this.watchEnabled=!0,this.scanNow().catch(s=>{console.error("[native-fusion] initial scan failed",s)}).finally(()=>{this.lifecycleRevision===e&&this.start()})}registerManagedSend(e){if(!e.canonicalMessageId)return;const s=e.sourceAgentType??e.agentType;if(s!=="codex"&&s!=="claude"&&s!=="opencode")return;const n=e.canonicalMessageId,t=this.pendingClaims.get(n),r=L(e.text);if(t&&(t.sessionId!==e.sessionId||t.sourceAgentType!==s||t.textHash!==r))throw new Error(`managed echo claim identity conflict: ${n}`);const u={sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:s,canonicalMessageId:e.canonicalMessageId,textHash:r,createdAt:t?.createdAt??Date.now(),sourceSessionKey:e.sourceSessionKey??t?.sourceSessionKey??null,managedEchoPolicy:e.managedEchoPolicy??"suppress_following",externalRunId:e.externalRunId??null};if(this.repository.upsertManagedClaim?.(u),this.pendingClaims.set(n,u),e.externalRunId&&e.sourceSessionKey){const h=`${s}:${e.sourceSessionKey}`,y=this.externalObservations.get(h)??[];y.push({sessionId:e.sessionId,sourceSessionKey:e.sourceSessionKey,runId:e.externalRunId,canonicalMessageId:e.canonicalMessageId,state:"queued",createdAt:Date.now(),lastPublishedAt:Date.now()}),this.externalObservations.set(h,y),this.emitExternalActivity(y[y.length-1],"queued"),this.ensureActiveScanning()}}cancelManagedSend(e){this.repository.removeManagedClaim?.(e),this.pendingClaims.delete(e)}noteManagedSourceSession(e,s,n){if(n){this.repository.updateManagedClaimSource?.(e,s,n);for(const t of this.pendingClaims.values())t.sessionId===e&&t.sourceAgentType===s&&(t.sourceSessionKey=n)}}handleSessionDeleted(e){for(const[s,n]of this.pendingClaims.entries())n.sessionId===e&&this.pendingClaims.delete(s);for(const[s,n]of this.suppressionWindows.entries())n.sessionId===e&&this.suppressionWindows.delete(s);for(const[s,n]of this.externalObservations.entries()){const t=n.filter(r=>r.sessionId!==e);t.length===0?this.externalObservations.delete(s):this.externalObservations.set(s,t)}this.repository.purgeSession(e)}ensureActiveScanning(e=!0){this.activeTimer||(this.activeTimer=setInterval(()=>{if(this.externalObservations.size===0&&this.pendingBoundaryFiles.size===0){this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null;return}this.scanActiveTargets()},ce),e&&this.scanActiveTargets())}scanActiveTargets(){const e=this.activeScanTargets();e===void 0&&this.scanPromise||this.scanNow(e).catch(s=>{console.error("[native-fusion] active observation scan failed",s)})}activeScanTargets(){const e=[...this.externalObservations.keys()];if(!e.some(s=>!this.sourceFiles.has(s)))return[...new Set([...this.pendingBoundaryFiles,...e.flatMap(s=>{const n=this.sourceFiles.get(s);return n?[n]:[]})])]}emitExternalActivity(e,s){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${s}-${e.lastPublishedAt}`,payload:{state:s==="queued"?"start":"heartbeat",sessionId:e.sessionId,runId:e.runId,seq:s==="queued"?0:1,runPhase:"thinking",canStop:!1,owner:"external",ownerLabel:"Codex Desktop",lifecycle:s}})}emitExternalTerminal(e,s="completed"){const n=s==="completed"?"final":s==="aborted"?"aborted":"error";this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${n}`,payload:{state:n,sessionId:e.sessionId,runId:e.runId,seq:2,...n==="error"?{message:"Codex Desktop turn failed"}:{},owner:"external",ownerLabel:"Codex Desktop"}}),this.externalTerminalHandler?.(e.sessionId)}pruneState(){const e=Date.now();for(const[s,n]of this.suppressionWindows.entries())n.endsAt>e||(this.repository.removeManagedSuppression?.(n.agentType,n.sourceSessionKey),this.suppressionWindows.delete(s))}async scanNow(e){if(this.client.getState()==="connected"){if(this.scanPromise){const s=this.scanPromise;if(e)for(const n of e)this.queuedTargetFiles.add(n);else this.fullScanQueued=!0;if(await s,this.fullScanQueued)return this.fullScanQueued=!1,this.queuedTargetFiles.clear(),this.scanNow();if(this.queuedTargetFiles.size>0){const n=[...this.queuedTargetFiles];return this.queuedTargetFiles.clear(),this.scanNow(n)}return}return this.scanPromise=this.runScanLoop(e).finally(()=>{this.scanPromise=null}),this.scanPromise}}async runScanLoop(e){let s=e?[...new Set(e)]:void 0;for(;;){if(await this.runScan(s),this.fullScanQueued){await F(),this.fullScanQueued=!1,this.queuedTargetFiles.clear(),s=void 0;continue}if(this.queuedTargetFiles.size>0){await F(),s=[...this.queuedTargetFiles],this.queuedTargetFiles.clear();continue}return}}async runScan(e){this.pruneState(),this.refreshExternalObservations();const s=C();for(const o of Object.keys(s.ignoredSources??{}))this.ignoredSources.add(o);s.ignoredSources=Object.fromEntries([...this.ignoredSources].sort().map(o=>[o,!0]));const n={...s.files},t=new Map;let r=!1;const u=o=>{const c=new Set,w=l=>{try{this.repository.commitEvents(l)}catch(a){if(!(a instanceof Q))throw a;const p=l.find(M=>M.sourceEventKey===a.sourceEventKey);if(!p)throw a;c.add(p.sourceEventKey),console.warn(`[native-fusion] quarantined conflicting native event ${p.sourceEventKey}`);const T=l.filter(M=>M!==p);T.length>0&&w(T)}};return w(o),c},h=e?[...e]:[...G(),...X(),...ee()].sort((o,c)=>k(c)-k(o));!e&&this.watchEnabled&&this.watcher.refresh(h,Z());let y=0,I=!1;for(;h.length>0||I&&this.queuedTargetFiles.size>0;){let o;if(I&&this.queuedTargetFiles.size>0){if(o=[...this.queuedTargetFiles].sort((i,d)=>k(d)-k(i))[0],o){this.queuedTargetFiles.delete(o);const i=h.indexOf(o);i>=0&&h.splice(i,1)}I=!1}else o=h.shift(),I=!0;if(!o)break;if(y>0&&y%le===0&&await F(),y+=1,!N.existsSync(o)){this.watchEnabled&&this.watcher.untrackFile(o),this.pendingBoundaryFiles.delete(o);for(const[i,d]of this.sourceFiles.entries())d===o&&this.sourceFiles.delete(i);continue}this.watchEnabled&&this.watcher.trackFile(o);const c=N.statSync(o),w=fe(o),l=o.endsWith(".opencode-session.json");let a=s.files[o];const p=`${String(c.dev)}:${String(c.ino)}:${String(c.birthtimeMs)}`;let T=!a;if(a&&!l&&(c.size<a.offset||a.fileIdentity&&a.fileIdentity!==p)&&(a=void 0,T=!0),!!(a&&a.fileIdentity===p&&a.mtimeMs===c.mtimeMs&&a.offset>=c.size&&a.sources?.length)&&a){for(const i of a.sources??[])this.sourceFiles.set(g(i.agentType,i.sourceSessionKey),o);a.classification==="live"&&a.awaitingUserBoundary?this.pendingBoundaryFiles.add(o):this.pendingBoundaryFiles.delete(o);continue}const m=a?.sources?.length&&!l?{status:"ready",createdAtMs:a.sessionCreatedAtMs??c.birthtimeMs,sources:a.sources}:J(o);if(m.status==="pending"){c.birthtimeMs>=s.fusionEpochMs&&this.pendingBoundaryFiles.add(o);continue}for(const i of m.sources)this.sourceFiles.set(g(i.agentType,i.sourceSessionKey),o);if(!l&&m.sources.length>0&&m.sources.every(i=>this.repository.isSourceDeleted?.(i.agentType,i.sourceSessionKey)||this.repository.isSourceInternal?.(i.agentType,i.sourceSessionKey)||this.ignoredSources.has(g(i.agentType,i.sourceSessionKey)))){n[o]={...a??{daemonInstallationId:s.daemonInstallationId,classification:m.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:m.createdAtMs,sourceBoundaryEstablished:{}},offset:c.size,mtimeMs:c.mtimeMs,fileIdentity:p};continue}if(!a)a={daemonInstallationId:s.daemonInstallationId,offset:0,mtimeMs:c.mtimeMs,classification:m.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:m.createdAtMs,sourceBoundaryEstablished:{}};else if(l&&a.mtimeMs===c.mtimeMs)continue;const E=l?0:a.offset;let S;try{if(S=l?ne(o,E):w?await ie(o,E):await te(o,E),!l&&S.nextOffset<E)throw new Error("native transcript cursor moved backwards")}catch(i){console.error("[native-fusion] target file scan failed; isolated for retry",i);continue}let A,v;if(l){const i=ye(S.events,a,s.fusionEpochMs,T,c.mtimeMs,S.nextOffset);A=i.events,v=i.cursor}else if(T&&a.classification==="historical")A=[],v={...a,offset:S.nextOffset,mtimeMs:c.mtimeMs,awaitingUserBoundary:!0};else{const i=he(S.events,a,S.nextOffset,c.mtimeMs);A=i.events,v=i.cursor}v.fileIdentity=p,v.sources=m.sources;const b=[],O=new Map,P=new Map;for(const i of A){if(this.ignoredSources.has(g(i.agentType,i.sourceSessionKey))||this.repository.isSourceDeleted?.(i.agentType,i.sourceSessionKey)||this.repository.isSourceInternal?.(i.agentType,i.sourceSessionKey))continue;const d=this.tryClaimManagedEcho(i);if(d){b.push(d.event),O.set(d.claimKey,d.suppression??null),d.suppression&&P.set(g(d.suppression.agentType,d.suppression.sourceSessionKey),{window:d.suppression,claimKey:d.claimKey});continue}const K=this.suppressionFor(i,P,O);if(K){b.push({...i,claimSessionId:K.sessionId,localDisposition:"managed_echo_suppressed"});continue}b.push(i)}const x=b.sort((i,d)=>i.ts-d.ts),_=new Set;try{for(let i=0;i<x.length;i+=q){const d=u(x.slice(i,i+q));for(const K of d)_.add(K);r=!0,i+q<x.length&&await F()}}catch(i){console.error("[native-fusion] target file commit failed; isolated for retry",i);continue}const $=new Set(x.filter(i=>_.has(i.sourceEventKey)).flatMap(i=>i.claimCanonicalMessageId?[i.claimCanonicalMessageId]:[]));for(const[i,d]of O)$.has(i)||(this.repository.completeManagedClaim?.(i,d??void 0),this.pendingClaims.delete(i),d&&this.suppressionWindows.set(g(d.agentType,d.sourceSessionKey),d));for(const i of S.events)this.ignoredSources.has(g(i.agentType,i.sourceSessionKey))||this.repository.isSourceDeleted?.(i.agentType,i.sourceSessionKey)||this.repository.isSourceInternal?.(i.agentType,i.sourceSessionKey)||i.title?.trim()&&i.titleSource==="agent_native"&&t.set(`${i.agentType}:${i.sourceSessionKey}`,i);for(const i of x)_.has(i.sourceEventKey)||this.observeExternalLifecycle(i);n[o]=v,s.files=n,B(s),v.classification==="live"&&v.awaitingUserBoundary?this.pendingBoundaryFiles.add(o):this.pendingBoundaryFiles.delete(o),x.length>0&&await R(this.client).catch(i=>{console.error("[native-fusion] index projection sync failed; retained for retry",i)})}r||this.repository.commitEvents([]);for(const o of t.values())this.repository.observeNativeTitle(o.agentType,o.sourceSessionKey,o.title??"");for(const o of this.repository.listSourceBindings()){if(o.agentType!=="codex")continue;const c=se(o.sourceSessionKey);c&&this.repository.observeNativeTitle("codex",o.sourceSessionKey,c)}s.files=n,B(s),this.pendingBoundaryFiles.size>0&&this.ensureActiveScanning(!1),this.repository.repairMissingUserBoundaries?.(),await R(this.client).catch(o=>{console.error("[native-fusion] index projection sync failed; retained for retry",o)})}refreshExternalObservations(){const e=Date.now();for(const[s,n]of this.externalObservations.entries()){const t=n.filter(u=>e-u.createdAt<D);if(t.length===0){this.externalObservations.delete(s);continue}this.externalObservations.set(s,t);const r=t[0];e-r.lastPublishedAt<ue||(r.lastPublishedAt=e,this.emitExternalActivity(r,r.state))}}tryClaimManagedEcho(e){const s=Date.now();for(const[n,t]of this.pendingClaims.entries()){if(t.sourceAgentType!==e.agentType||e.role!=="user"||!t.sourceSessionKey||t.sourceSessionKey!==e.sourceSessionKey)continue;const r=t.externalRunId?D:re;if(Math.abs(e.ts-t.createdAt)>r||L(e.payload)!==t.textHash)continue;const u=t.managedEchoPolicy==="suppress_following"?{sessionId:t.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,claimSourceEventKey:e.sourceEventKey,startedAt:e.ts,endsAt:Math.max(s,e.ts)+ae}:void 0;return{claimKey:n,suppression:u,event:{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:t.canonicalMessageId,claimSessionId:t.sessionId,managedEchoPolicy:t.managedEchoPolicy}}}return null}observeExternalLifecycle(e){const s=`${e.agentType}:${e.sourceSessionKey}`,n=this.externalObservations.get(s);if(!n?.length)return;const t=n[0];if(e.claimCanonicalMessageId===t.canonicalMessageId&&t.state==="queued"&&(this.repository.updateMessageDeliveryState(t.sessionId,t.canonicalMessageId,"executing"),this.externalClaimHandler?.(t.sessionId,t.canonicalMessageId),t.state="running",t.lastPublishedAt=Date.now(),this.emitExternalActivity(t,"running")),!e.terminal||t.state!=="running")return;const r=e.terminalState??"completed";this.repository.updateMessageDeliveryState(t.sessionId,t.canonicalMessageId,r==="aborted"?"cancelled":r==="failed"?"failed":"completed"),this.emitExternalTerminal(t,r),n.shift(),n.length===0?this.externalObservations.delete(s):(this.externalObservations.set(s,n),n[0].lastPublishedAt=Date.now(),this.emitExternalActivity(n[0],"queued"))}suppressionFor(e,s,n){const t=g(e.agentType,e.sourceSessionKey),r=s.get(t),u=r?.window??this.suppressionWindows.get(t);return!u||e.ts<u.startedAt?null:e.ts>u.endsAt?(this.clearSuppression(t,u,r,s,n),null):e.role==="user"?(e.sourceEventKey===u.claimSourceEventKey||this.clearSuppression(t,u,r,s,n),null):u}clearSuppression(e,s,n,t,r){if(n){t.delete(e),r.set(n.claimKey,null);return}this.repository.removeManagedSuppression?.(s.agentType,s.sourceSessionKey),this.suppressionWindows.delete(e)}}function L(f){return W.createHash("sha256").update(de(f)).digest("hex")}function g(f,e){return`${f}:${e}`}function he(f,e,s,n){if(!e.awaitingUserBoundary)return{events:f,cursor:{...e,offset:s,mtimeMs:n}};if(f.length===0)return{events:[],cursor:{...e,offset:s,mtimeMs:n,awaitingUserBoundary:!0}};const t=f.findIndex(r=>r.role==="user");return t<0?{events:[],cursor:{...e,mtimeMs:n,awaitingUserBoundary:!0}}:{events:f.slice(t),cursor:{...e,offset:s,mtimeMs:n,awaitingUserBoundary:!1}}}function ye(f,e,s,n,t,r){const u={...e.sourceBoundaryEstablished??{}},h=e.eventWatermarkTs??-1,y=new Set(e.eventKeysAtWatermark??[]),I=n?f:f.filter(l=>l.ts>h||l.ts===h&&!y.has(l.sourceEventKey)),o=[];for(const l of I){const a=l.sourceSessionKey;if(u[a]){o.push(l);continue}!(!n||(l.sourceSessionCreatedAtMs??0)>=s)||l.role!=="user"||(u[a]=!0,o.push(l))}const c=f.reduce((l,a)=>Math.max(l,a.ts),h),w=new Set(c===h?e.eventKeysAtWatermark??[]:[]);for(const l of f)l.ts===c&&w.add(l.sourceEventKey);return{events:o,cursor:{...e,offset:r,mtimeMs:t,eventWatermarkTs:c>=0?c:void 0,eventKeysAtWatermark:c>=0?[...w]:void 0,sourceBoundaryEstablished:u}}}export{Me as NativeSessionFusionService};
|
|
1
|
+
import $ from"node:crypto";import _ from"node:fs";import{listSessionRecords as H}from"../session/store.js";import{knownInternalNativeSources as z,knownJournalInternalNativeSources as U,registerInternalNativeSource as j}from"./source-visibility.js";import{publishPendingSessionIndexes as R}from"../session/index-publisher.js";import{NativeFusionFileWatcher as V}from"./file-watcher.js";import{NativeEventIdentityConflictError as Q,NativeFusionLocalRepository as Y}from"./local-repository.js";import{inspectNativeSessionFile as J,listClaudeTranscriptFiles as X,listCodexRolloutFiles as G,listNativeTranscriptRoots as Z,listOpenCodeSessionFiles as ee,lookupCodexThreadName as se,parseClaudeTranscriptChunkCooperatively as te,parseCodexRolloutChunkCooperatively as ie,parseOpenCodeSessionFile as ne}from"./parsers.js";import{loadNativeScannerState as C,saveNativeScannerState as B}from"./state.js";import{nativeFileIdentity as oe,normalizeNativeFileIdentity as re}from"./file-identity.js";const ae=5*6e4,ce=2*6e4,le=30*6e4,ue=2e3,de=1,q=16,fe=3e4,D=240*6e4;function he(f){return f.replace(/\r\n/g,`
|
|
2
|
+
`).trim()}function F(){return new Promise(f=>setImmediate(f))}function ye(f){return f.split(/[\\/]+/).includes(".codex")}function k(f){try{return _.statSync(f).mtimeMs}catch{return 0}}class be{client;repository;timer=null;activeTimer=null;scanPromise=null;queuedTargetFiles=new Set;pendingBoundaryFiles=new Set;sourceFiles=new Map;fullScanQueued=!1;watchEnabled=!1;lifecycleRevision=0;watcher=new V(e=>{this.scanNow(e).catch(s=>{console.error("[native-fusion] watched file scan failed",s)})});pendingClaims;suppressionWindows;externalObservations=new Map;externalTerminalHandler=null;externalClaimHandler=null;ignoredSources=new Set;constructor(e,s=new Y){this.client=e,this.repository=s;const n=C();for(const t of[...z(H()),...U(n.daemonInstallationId)])j(t.sessionId,t.agentType,t.sourceSessionKey);this.ignoredSources=new Set(Object.keys(C().ignoredSources??{})),this.pendingClaims=new Map((this.repository.listManagedClaims?.()??[]).map(t=>[t.canonicalMessageId,t])),this.suppressionWindows=new Map((this.repository.listManagedSuppressions?.()??[]).map(t=>[g(t.agentType,t.sourceSessionKey),t]))}ignoreNativeSource(e,s){if(!["codex","claude","opencode"].includes(e)||!s)return;const n=g(e,s);if(this.ignoredSources.has(n))return;this.ignoredSources.add(n);const t=C();for(const r of Object.keys(t.ignoredSources??{}))this.ignoredSources.add(r);t.ignoredSources=Object.fromEntries([...this.ignoredSources].sort().map(r=>[r,!0])),B(t)}start(){this.timer||(this.timer=setInterval(()=>{this.scanNow().catch(e=>{console.error("[native-fusion] periodic scan failed",e)})},ae))}stop(){this.lifecycleRevision+=1,this.timer&&clearInterval(this.timer),this.timer=null,this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null,this.pendingBoundaryFiles.clear(),this.watchEnabled=!1,this.watcher.stop()}setExternalTerminalHandler(e){this.externalTerminalHandler=e}setExternalClaimHandler(e){if(this.externalClaimHandler=e,!!e)for(const s of this.externalObservations.values())for(const n of s)n.state==="running"&&e(n.sessionId,n.canonicalMessageId)}restoreExternalObservation(e){if(!e.sessionId||!e.sourceSessionKey||!e.runId||!e.canonicalMessageId||!Number.isFinite(e.createdAt))return!1;const s=`codex:${e.sourceSessionKey}`,n=this.externalObservations.get(s)??[];if(n.some(u=>u.runId===e.runId))return!0;const t=Date.now(),r={...e,state:this.pendingClaims.has(e.canonicalMessageId)?"queued":"running",createdAt:Math.min(e.createdAt,t),lastPublishedAt:t};return n.push(r),n.sort((u,h)=>u.createdAt-h.createdAt),this.externalObservations.set(s,n),this.emitExternalActivity(r,r.state),r.state==="running"&&this.externalClaimHandler?.(r.sessionId,r.canonicalMessageId),this.ensureActiveScanning(!1),!0}handleConnected(){const e=++this.lifecycleRevision;this.watchEnabled=!0,this.scanNow().catch(s=>{console.error("[native-fusion] initial scan failed",s)}).finally(()=>{this.lifecycleRevision===e&&this.start()})}registerManagedSend(e){if(!e.canonicalMessageId)return;const s=e.sourceAgentType??e.agentType;if(s!=="codex"&&s!=="claude"&&s!=="opencode")return;const n=e.canonicalMessageId,t=this.pendingClaims.get(n),r=L(e.text);if(t&&(t.sessionId!==e.sessionId||t.sourceAgentType!==s||t.textHash!==r))throw new Error(`managed echo claim identity conflict: ${n}`);const u={sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:s,canonicalMessageId:e.canonicalMessageId,textHash:r,createdAt:t?.createdAt??Date.now(),sourceSessionKey:e.sourceSessionKey??t?.sourceSessionKey??null,managedEchoPolicy:e.managedEchoPolicy??"suppress_following",externalRunId:e.externalRunId??null};if(this.repository.upsertManagedClaim?.(u),this.pendingClaims.set(n,u),e.externalRunId&&e.sourceSessionKey){const h=`${s}:${e.sourceSessionKey}`,y=this.externalObservations.get(h)??[];y.push({sessionId:e.sessionId,sourceSessionKey:e.sourceSessionKey,runId:e.externalRunId,canonicalMessageId:e.canonicalMessageId,state:"queued",createdAt:Date.now(),lastPublishedAt:Date.now()}),this.externalObservations.set(h,y),this.emitExternalActivity(y[y.length-1],"queued"),this.ensureActiveScanning()}}cancelManagedSend(e){this.repository.removeManagedClaim?.(e),this.pendingClaims.delete(e)}noteManagedSourceSession(e,s,n){if(n){this.repository.updateManagedClaimSource?.(e,s,n);for(const t of this.pendingClaims.values())t.sessionId===e&&t.sourceAgentType===s&&(t.sourceSessionKey=n)}}handleSessionDeleted(e){for(const[s,n]of this.pendingClaims.entries())n.sessionId===e&&this.pendingClaims.delete(s);for(const[s,n]of this.suppressionWindows.entries())n.sessionId===e&&this.suppressionWindows.delete(s);for(const[s,n]of this.externalObservations.entries()){const t=n.filter(r=>r.sessionId!==e);t.length===0?this.externalObservations.delete(s):this.externalObservations.set(s,t)}this.repository.purgeSession(e)}ensureActiveScanning(e=!0){this.activeTimer||(this.activeTimer=setInterval(()=>{if(this.externalObservations.size===0&&this.pendingBoundaryFiles.size===0){this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null;return}this.scanActiveTargets()},ue),e&&this.scanActiveTargets())}scanActiveTargets(){const e=this.activeScanTargets();e===void 0&&this.scanPromise||this.scanNow(e).catch(s=>{console.error("[native-fusion] active observation scan failed",s)})}activeScanTargets(){const e=[...this.externalObservations.keys()];if(!e.some(s=>!this.sourceFiles.has(s)))return[...new Set([...this.pendingBoundaryFiles,...e.flatMap(s=>{const n=this.sourceFiles.get(s);return n?[n]:[]})])]}emitExternalActivity(e,s){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${s}-${e.lastPublishedAt}`,payload:{state:s==="queued"?"start":"heartbeat",sessionId:e.sessionId,runId:e.runId,seq:s==="queued"?0:1,runPhase:"thinking",canStop:!1,owner:"external",ownerLabel:"Codex Desktop",lifecycle:s}})}emitExternalTerminal(e,s="completed"){const n=s==="completed"?"final":s==="aborted"?"aborted":"error";this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${n}`,payload:{state:n,sessionId:e.sessionId,runId:e.runId,seq:2,...n==="error"?{message:"Codex Desktop turn failed"}:{},owner:"external",ownerLabel:"Codex Desktop"}}),this.externalTerminalHandler?.(e.sessionId)}pruneState(){const e=Date.now();for(const[s,n]of this.suppressionWindows.entries())n.endsAt>e||(this.repository.removeManagedSuppression?.(n.agentType,n.sourceSessionKey),this.suppressionWindows.delete(s))}async scanNow(e){if(this.client.getState()==="connected"){if(this.scanPromise){const s=this.scanPromise;if(e)for(const n of e)this.queuedTargetFiles.add(n);else this.fullScanQueued=!0;if(await s,this.fullScanQueued)return this.fullScanQueued=!1,this.queuedTargetFiles.clear(),this.scanNow();if(this.queuedTargetFiles.size>0){const n=[...this.queuedTargetFiles];return this.queuedTargetFiles.clear(),this.scanNow(n)}return}return this.scanPromise=this.runScanLoop(e).finally(()=>{this.scanPromise=null}),this.scanPromise}}async runScanLoop(e){let s=e?[...new Set(e)]:void 0;for(;;){if(await this.runScan(s),this.fullScanQueued){await F(),this.fullScanQueued=!1,this.queuedTargetFiles.clear(),s=void 0;continue}if(this.queuedTargetFiles.size>0){await F(),s=[...this.queuedTargetFiles],this.queuedTargetFiles.clear();continue}return}}async runScan(e){this.pruneState(),this.refreshExternalObservations();const s=C();for(const o of Object.keys(s.ignoredSources??{}))this.ignoredSources.add(o);s.ignoredSources=Object.fromEntries([...this.ignoredSources].sort().map(o=>[o,!0]));const n={...s.files},t=new Map;let r=!1;const u=o=>{const c=new Set,w=l=>{try{this.repository.commitEvents(l)}catch(a){if(!(a instanceof Q))throw a;const p=l.find(I=>I.sourceEventKey===a.sourceEventKey);if(!p)throw a;c.add(p.sourceEventKey),console.warn(`[native-fusion] quarantined conflicting native event ${p.sourceEventKey}`);const x=l.filter(I=>I!==p);x.length>0&&w(x)}};return w(o),c},h=e?[...e]:[...G(),...X(),...ee()].sort((o,c)=>k(c)-k(o));!e&&this.watchEnabled&&this.watcher.refresh(h,Z());let y=0,T=!1;for(;h.length>0||T&&this.queuedTargetFiles.size>0;){let o;if(T&&this.queuedTargetFiles.size>0){if(o=[...this.queuedTargetFiles].sort((i,d)=>k(d)-k(i))[0],o){this.queuedTargetFiles.delete(o);const i=h.indexOf(o);i>=0&&h.splice(i,1)}T=!1}else o=h.shift(),T=!0;if(!o)break;if(y>0&&y%de===0&&await F(),y+=1,!_.existsSync(o)){this.watchEnabled&&this.watcher.untrackFile(o),this.pendingBoundaryFiles.delete(o);for(const[i,d]of this.sourceFiles.entries())d===o&&this.sourceFiles.delete(i);continue}this.watchEnabled&&this.watcher.trackFile(o);const c=_.statSync(o),w=ye(o),l=o.endsWith(".opencode-session.json");let a=s.files[o];const p=oe(c),x=a?.fileIdentity?re(a.fileIdentity):void 0;let I=!a;if(a&&!l&&(c.size<a.offset||x&&x!==p)&&(a=void 0,I=!0),!!(a&&x===p&&a.mtimeMs===c.mtimeMs&&a.offset>=c.size&&a.sources?.length)&&a){for(const i of a.sources??[])this.sourceFiles.set(g(i.agentType,i.sourceSessionKey),o);a.classification==="live"&&a.awaitingUserBoundary?this.pendingBoundaryFiles.add(o):this.pendingBoundaryFiles.delete(o);continue}const m=a?.sources?.length&&!l?{status:"ready",createdAtMs:a.sessionCreatedAtMs??c.birthtimeMs,sources:a.sources}:J(o);if(m.status==="pending"){c.birthtimeMs>=s.fusionEpochMs&&this.pendingBoundaryFiles.add(o);continue}for(const i of m.sources)this.sourceFiles.set(g(i.agentType,i.sourceSessionKey),o);if(!l&&m.sources.length>0&&m.sources.every(i=>this.repository.isSourceDeleted?.(i.agentType,i.sourceSessionKey)||this.repository.isSourceInternal?.(i.agentType,i.sourceSessionKey)||this.ignoredSources.has(g(i.agentType,i.sourceSessionKey)))){n[o]={...a??{daemonInstallationId:s.daemonInstallationId,classification:m.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:m.createdAtMs,sourceBoundaryEstablished:{}},offset:c.size,mtimeMs:c.mtimeMs,fileIdentity:p};continue}if(!a)a={daemonInstallationId:s.daemonInstallationId,offset:0,mtimeMs:c.mtimeMs,classification:m.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:m.createdAtMs,sourceBoundaryEstablished:{}};else if(l&&a.mtimeMs===c.mtimeMs)continue;const E=l?0:a.offset;let S;try{if(S=l?ne(o,E):w?await ie(o,E):await te(o,E),!l&&S.nextOffset<E)throw new Error("native transcript cursor moved backwards")}catch(i){console.error("[native-fusion] target file scan failed; isolated for retry",i);continue}let A,v;if(l){const i=ge(S.events,a,s.fusionEpochMs,I,c.mtimeMs,S.nextOffset);A=i.events,v=i.cursor}else if(I&&a.classification==="historical")A=[],v={...a,offset:S.nextOffset,mtimeMs:c.mtimeMs,awaitingUserBoundary:!0};else{const i=pe(S.events,a,S.nextOffset,c.mtimeMs);A=i.events,v=i.cursor}v.fileIdentity=p,v.sources=m.sources;const K=[],O=new Map,P=new Map;for(const i of A){if(this.ignoredSources.has(g(i.agentType,i.sourceSessionKey))||this.repository.isSourceDeleted?.(i.agentType,i.sourceSessionKey)||this.repository.isSourceInternal?.(i.agentType,i.sourceSessionKey))continue;const d=this.tryClaimManagedEcho(i);if(d){K.push(d.event),O.set(d.claimKey,d.suppression??null),d.suppression&&P.set(g(d.suppression.agentType,d.suppression.sourceSessionKey),{window:d.suppression,claimKey:d.claimKey});continue}const b=this.suppressionFor(i,P,O);if(b){K.push({...i,claimSessionId:b.sessionId,localDisposition:"managed_echo_suppressed"});continue}K.push(i)}const M=K.sort((i,d)=>i.ts-d.ts),N=new Set;try{for(let i=0;i<M.length;i+=q){const d=u(M.slice(i,i+q));for(const b of d)N.add(b);r=!0,i+q<M.length&&await F()}}catch(i){console.error("[native-fusion] target file commit failed; isolated for retry",i);continue}const W=new Set(M.filter(i=>N.has(i.sourceEventKey)).flatMap(i=>i.claimCanonicalMessageId?[i.claimCanonicalMessageId]:[]));for(const[i,d]of O)W.has(i)||(this.repository.completeManagedClaim?.(i,d??void 0),this.pendingClaims.delete(i),d&&this.suppressionWindows.set(g(d.agentType,d.sourceSessionKey),d));for(const i of S.events)this.ignoredSources.has(g(i.agentType,i.sourceSessionKey))||this.repository.isSourceDeleted?.(i.agentType,i.sourceSessionKey)||this.repository.isSourceInternal?.(i.agentType,i.sourceSessionKey)||i.title?.trim()&&i.titleSource==="agent_native"&&t.set(`${i.agentType}:${i.sourceSessionKey}`,i);for(const i of M)N.has(i.sourceEventKey)||this.observeExternalLifecycle(i);n[o]=v,s.files=n,B(s),v.classification==="live"&&v.awaitingUserBoundary?this.pendingBoundaryFiles.add(o):this.pendingBoundaryFiles.delete(o),M.length>0&&await R(this.client).catch(i=>{console.error("[native-fusion] index projection sync failed; retained for retry",i)})}r||this.repository.commitEvents([]);for(const o of t.values())this.repository.observeNativeTitle(o.agentType,o.sourceSessionKey,o.title??"");for(const o of this.repository.listSourceBindings()){if(o.agentType!=="codex")continue;const c=se(o.sourceSessionKey);c&&this.repository.observeNativeTitle("codex",o.sourceSessionKey,c)}s.files=n,B(s),this.pendingBoundaryFiles.size>0&&this.ensureActiveScanning(!1),this.repository.repairMissingUserBoundaries?.(),await R(this.client).catch(o=>{console.error("[native-fusion] index projection sync failed; retained for retry",o)})}refreshExternalObservations(){const e=Date.now();for(const[s,n]of this.externalObservations.entries()){const t=n.filter(u=>e-u.createdAt<D);if(t.length===0){this.externalObservations.delete(s);continue}this.externalObservations.set(s,t);const r=t[0];e-r.lastPublishedAt<fe||(r.lastPublishedAt=e,this.emitExternalActivity(r,r.state))}}tryClaimManagedEcho(e){const s=Date.now();for(const[n,t]of this.pendingClaims.entries()){if(t.sourceAgentType!==e.agentType||e.role!=="user"||!t.sourceSessionKey||t.sourceSessionKey!==e.sourceSessionKey)continue;const r=t.externalRunId?D:ce;if(Math.abs(e.ts-t.createdAt)>r||L(e.payload)!==t.textHash)continue;const u=t.managedEchoPolicy==="suppress_following"?{sessionId:t.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,claimSourceEventKey:e.sourceEventKey,startedAt:e.ts,endsAt:Math.max(s,e.ts)+le}:void 0;return{claimKey:n,suppression:u,event:{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:t.canonicalMessageId,claimSessionId:t.sessionId,managedEchoPolicy:t.managedEchoPolicy}}}return null}observeExternalLifecycle(e){const s=`${e.agentType}:${e.sourceSessionKey}`,n=this.externalObservations.get(s);if(!n?.length)return;const t=n[0];if(e.claimCanonicalMessageId===t.canonicalMessageId&&t.state==="queued"&&(this.repository.updateMessageDeliveryState(t.sessionId,t.canonicalMessageId,"executing"),this.externalClaimHandler?.(t.sessionId,t.canonicalMessageId),t.state="running",t.lastPublishedAt=Date.now(),this.emitExternalActivity(t,"running")),!e.terminal||t.state!=="running")return;const r=e.terminalState??"completed";this.repository.updateMessageDeliveryState(t.sessionId,t.canonicalMessageId,r==="aborted"?"cancelled":r==="failed"?"failed":"completed"),this.emitExternalTerminal(t,r),n.shift(),n.length===0?this.externalObservations.delete(s):(this.externalObservations.set(s,n),n[0].lastPublishedAt=Date.now(),this.emitExternalActivity(n[0],"queued"))}suppressionFor(e,s,n){const t=g(e.agentType,e.sourceSessionKey),r=s.get(t),u=r?.window??this.suppressionWindows.get(t);return!u||e.ts<u.startedAt?null:e.ts>u.endsAt?(this.clearSuppression(t,u,r,s,n),null):e.role==="user"?(e.sourceEventKey===u.claimSourceEventKey||this.clearSuppression(t,u,r,s,n),null):u}clearSuppression(e,s,n,t,r){if(n){t.delete(e),r.set(n.claimKey,null);return}this.repository.removeManagedSuppression?.(s.agentType,s.sourceSessionKey),this.suppressionWindows.delete(e)}}function L(f){return $.createHash("sha256").update(he(f)).digest("hex")}function g(f,e){return`${f}:${e}`}function pe(f,e,s,n){if(!e.awaitingUserBoundary)return{events:f,cursor:{...e,offset:s,mtimeMs:n}};if(f.length===0)return{events:[],cursor:{...e,offset:s,mtimeMs:n,awaitingUserBoundary:!0}};const t=f.findIndex(r=>r.role==="user");return t<0?{events:[],cursor:{...e,mtimeMs:n,awaitingUserBoundary:!0}}:{events:f.slice(t),cursor:{...e,offset:s,mtimeMs:n,awaitingUserBoundary:!1}}}function ge(f,e,s,n,t,r){const u={...e.sourceBoundaryEstablished??{}},h=e.eventWatermarkTs??-1,y=new Set(e.eventKeysAtWatermark??[]),T=n?f:f.filter(l=>l.ts>h||l.ts===h&&!y.has(l.sourceEventKey)),o=[];for(const l of T){const a=l.sourceSessionKey;if(u[a]){o.push(l);continue}!(!n||(l.sourceSessionCreatedAtMs??0)>=s)||l.role!=="user"||(u[a]=!0,o.push(l))}const c=f.reduce((l,a)=>Math.max(l,a.ts),h),w=new Set(c===h?e.eventKeysAtWatermark??[]:[]);for(const l of f)l.ts===c&&w.add(l.sourceEventKey);return{events:o,cursor:{...e,offset:r,mtimeMs:t,eventWatermarkTs:c>=0?c:void 0,eventKeysAtWatermark:c>=0?[...w]:void 0,sourceBoundaryEstablished:u}}}export{be as NativeSessionFusionService};
|