@agimon-ai/doompi-runner 0.0.1-alpha.71 → 0.0.1-alpha.72
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/constants/rmuxBackend.cjs +1 -1
- package/dist/constants/rmuxBackend.cjs.map +1 -1
- package/dist/constants/rmuxBackend.mjs +1 -1
- package/dist/constants/rmuxBackend.mjs.map +1 -1
- package/dist/constants/spawner.cjs.map +1 -1
- package/dist/constants/spawner.mjs.map +1 -1
- package/dist/services/rmuxBackend/index.cjs +1 -1
- package/dist/services/rmuxBackend/index.cjs.map +1 -1
- package/dist/services/rmuxBackend/index.d.cts.map +1 -1
- package/dist/services/rmuxBackend/index.d.mts.map +1 -1
- package/dist/services/rmuxBackend/index.mjs +1 -1
- package/dist/services/rmuxBackend/index.mjs.map +1 -1
- package/dist/services/runnerRegistry/index.cjs +1 -1
- package/dist/services/runnerRegistry/index.cjs.map +1 -1
- package/dist/services/runnerRegistry/index.d.cts.map +1 -1
- package/dist/services/runnerRegistry/index.d.mts.map +1 -1
- package/dist/services/runnerRegistry/index.mjs +1 -1
- package/dist/services/runnerRegistry/index.mjs.map +1 -1
- package/dist/services/runnerSupervisor/index.cjs +1 -1
- package/dist/services/runnerSupervisor/index.cjs.map +1 -1
- package/dist/services/runnerSupervisor/index.mjs +1 -1
- package/dist/services/runnerSupervisor/index.mjs.map +1 -1
- package/dist/services/spawner/index.cjs +1 -1
- package/dist/services/spawner/index.cjs.map +1 -1
- package/dist/services/spawner/index.mjs +1 -1
- package/dist/services/spawner/index.mjs.map +1 -1
- package/dist/types/webRunnerLog.cjs +1 -1
- package/dist/types/webRunnerLog.cjs.map +1 -1
- package/dist/types/webRunnerLog.mjs +1 -1
- package/dist/types/webRunnerLog.mjs.map +1 -1
- package/package.json +21 -21
- package/src/constants/rmuxBackend.ts +5 -0
- package/src/constants/spawner.ts +2 -0
- package/src/types/webRunnerLog.ts +7 -5
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=/^doom-runner-[A-Za-z0-9_-]+$/,t={NO_COLOR:`1`,CI:`1`};exports.LOG_DRAIN_TIMEOUT_MS=2e3,exports.NON_INTERACTIVE_ENV=t,exports.OWNED_TARGET_PATTERN=e,exports.PANE_STATE_FORMAT=`#{pane_dead}:#{pane_dead_status}:#{session_name}`,exports.RMUX_BINARY_ENV=`DOOMPI_RMUX_BINARY`,exports.SESSION_PREFIX=`doom-runner-`,exports.STOP_CLOSE_TIMEOUT_MS=1e3,exports.STOP_GRACE_MS=3e3;
|
|
1
|
+
const e=/^doom-runner-[A-Za-z0-9_-]+$/,t={NO_COLOR:`1`,CI:`1`};exports.LOG_DRAIN_TIMEOUT_MS=2e3,exports.NON_INTERACTIVE_ENV=t,exports.OWNED_TARGET_PATTERN=e,exports.PANE_PID_FORMAT=`#{pane_pid}`,exports.PANE_STATE_FORMAT=`#{pane_dead}:#{pane_dead_status}:#{session_name}`,exports.RMUX_BINARY_ENV=`DOOMPI_RMUX_BINARY`,exports.SESSION_PREFIX=`doom-runner-`,exports.STOP_CLOSE_TIMEOUT_MS=1e3,exports.STOP_GRACE_MS=3e3;
|
|
2
2
|
//# sourceMappingURL=rmuxBackend.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rmuxBackend.cjs","names":[],"sources":["../../src/constants/rmuxBackend.ts"],"sourcesContent":["export const RMUX_BINARY_ENV = 'DOOMPI_RMUX_BINARY';\nexport const POLL_MS = 100;\nexport const LOG_DRAIN_TIMEOUT_MS = 2_000;\nexport const LOG_DRAIN_POLL_MS = 25;\nexport const PANE_STATE_FORMAT = '#{pane_dead}:#{pane_dead_status}:#{session_name}';\nexport const STOP_GRACE_MS = 3_000;\nexport const STOP_CLOSE_TIMEOUT_MS = 1_000;\nexport const EXECUTABLE_MODE = 0o755;\nexport const SESSION_PREFIX = 'doom-runner-';\nexport const OWNED_TARGET_PATTERN = /^doom-runner-[A-Za-z0-9_-]+$/;\nexport const SOCKET_HASH_LENGTH = 12;\nexport const NON_INTERACTIVE_ENV = { NO_COLOR: '1', CI: '1' };\n"],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"rmuxBackend.cjs","names":[],"sources":["../../src/constants/rmuxBackend.ts"],"sourcesContent":["export const RMUX_BINARY_ENV = 'DOOMPI_RMUX_BINARY';\nexport const POLL_MS = 100;\nexport const LOG_DRAIN_TIMEOUT_MS = 2_000;\n/**\n * Fallback interval for a missed log-completion event or a lost session.\n * The retained pane lets the sink write its marker before session cleanup.\n */\nexport const LOG_DRAIN_POLL_MS = 25;\nexport const PANE_STATE_FORMAT = '#{pane_dead}:#{pane_dead_status}:#{session_name}';\nexport const PANE_PID_FORMAT = '#{pane_pid}';\nexport const STOP_GRACE_MS = 3_000;\nexport const STOP_CLOSE_TIMEOUT_MS = 1_000;\nexport const EXECUTABLE_MODE = 0o755;\nexport const SESSION_PREFIX = 'doom-runner-';\nexport const OWNED_TARGET_PATTERN = /^doom-runner-[A-Za-z0-9_-]+$/;\nexport const SOCKET_HASH_LENGTH = 12;\nexport const NON_INTERACTIVE_ENV = { NO_COLOR: '1', CI: '1' };\n"],"mappings":"AAAA,MAca,EAAuB,+BAEvB,EAAsB,CAAE,SAAU,IAAK,GAAI,GAAI"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=`DOOMPI_RMUX_BINARY`,t=2e3,n=`#{pane_dead}:#{pane_dead_status}:#{session_name}`,r=3e3,
|
|
1
|
+
const e=`DOOMPI_RMUX_BINARY`,t=2e3,n=`#{pane_dead}:#{pane_dead_status}:#{session_name}`,r=`#{pane_pid}`,i=3e3,a=1e3,o=`doom-runner-`,s=/^doom-runner-[A-Za-z0-9_-]+$/,c={NO_COLOR:`1`,CI:`1`};export{t as LOG_DRAIN_TIMEOUT_MS,c as NON_INTERACTIVE_ENV,s as OWNED_TARGET_PATTERN,r as PANE_PID_FORMAT,n as PANE_STATE_FORMAT,e as RMUX_BINARY_ENV,o as SESSION_PREFIX,a as STOP_CLOSE_TIMEOUT_MS,i as STOP_GRACE_MS};
|
|
2
2
|
//# sourceMappingURL=rmuxBackend.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rmuxBackend.mjs","names":[],"sources":["../../src/constants/rmuxBackend.ts"],"sourcesContent":["export const RMUX_BINARY_ENV = 'DOOMPI_RMUX_BINARY';\nexport const POLL_MS = 100;\nexport const LOG_DRAIN_TIMEOUT_MS = 2_000;\nexport const LOG_DRAIN_POLL_MS = 25;\nexport const PANE_STATE_FORMAT = '#{pane_dead}:#{pane_dead_status}:#{session_name}';\nexport const STOP_GRACE_MS = 3_000;\nexport const STOP_CLOSE_TIMEOUT_MS = 1_000;\nexport const EXECUTABLE_MODE = 0o755;\nexport const SESSION_PREFIX = 'doom-runner-';\nexport const OWNED_TARGET_PATTERN = /^doom-runner-[A-Za-z0-9_-]+$/;\nexport const SOCKET_HASH_LENGTH = 12;\nexport const NON_INTERACTIVE_ENV = { NO_COLOR: '1', CI: '1' };\n"],"mappings":"AAAA,MAAa,EAAkB,qBAElB,EAAuB,
|
|
1
|
+
{"version":3,"file":"rmuxBackend.mjs","names":[],"sources":["../../src/constants/rmuxBackend.ts"],"sourcesContent":["export const RMUX_BINARY_ENV = 'DOOMPI_RMUX_BINARY';\nexport const POLL_MS = 100;\nexport const LOG_DRAIN_TIMEOUT_MS = 2_000;\n/**\n * Fallback interval for a missed log-completion event or a lost session.\n * The retained pane lets the sink write its marker before session cleanup.\n */\nexport const LOG_DRAIN_POLL_MS = 25;\nexport const PANE_STATE_FORMAT = '#{pane_dead}:#{pane_dead_status}:#{session_name}';\nexport const PANE_PID_FORMAT = '#{pane_pid}';\nexport const STOP_GRACE_MS = 3_000;\nexport const STOP_CLOSE_TIMEOUT_MS = 1_000;\nexport const EXECUTABLE_MODE = 0o755;\nexport const SESSION_PREFIX = 'doom-runner-';\nexport const OWNED_TARGET_PATTERN = /^doom-runner-[A-Za-z0-9_-]+$/;\nexport const SOCKET_HASH_LENGTH = 12;\nexport const NON_INTERACTIVE_ENV = { NO_COLOR: '1', CI: '1' };\n"],"mappings":"AAAA,MAAa,EAAkB,qBAElB,EAAuB,IAMvB,EAAoB,mDACpB,EAAkB,cAClB,EAAgB,IAChB,EAAwB,IAExB,EAAiB,eACjB,EAAuB,+BAEvB,EAAsB,CAAE,SAAU,IAAK,GAAI,GAAI"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawner.cjs","names":[],"sources":["../../src/constants/spawner.ts"],"sourcesContent":["export const SHELL = '/bin/bash';\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"spawner.cjs","names":[],"sources":["../../src/constants/spawner.ts"],"sourcesContent":["export const SHELL = '/bin/bash';\n/** Allow the supervisor's final pipe bytes to arrive after its exit event. */\nexport const EXIT_OUTPUT_DRAIN_MS = 250;\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawner.mjs","names":[],"sources":["../../src/constants/spawner.ts"],"sourcesContent":["export const SHELL = '/bin/bash';\n"],"mappings":"AAAA,MAAa,EAAQ"}
|
|
1
|
+
{"version":3,"file":"spawner.mjs","names":[],"sources":["../../src/constants/spawner.ts"],"sourcesContent":["export const SHELL = '/bin/bash';\n/** Allow the supervisor's final pipe bytes to arrive after its exit event. */\nexport const EXIT_OUTPUT_DRAIN_MS = 250;\n"],"mappings":"AAAA,MAAa,EAAQ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../runnerConfig/index.cjs"),n=require("../../constants/rmuxBackend.cjs"),r=require("../runnerSupervisor/index.cjs");let i=require("node:path");i=e.__toESM(i,1);let a=require("node:crypto"),o=require("node:fs");o=e.__toESM(o,1);let s=require("node:module"),c=require("@rmux/sdk");const l={"darwin-arm64":`@agimon-ai/doompi-runner-rmux-darwin-arm64`,"darwin-x64":`@agimon-ai/doompi-runner-rmux-darwin-x64`,"linux-arm64":`@agimon-ai/doompi-runner-rmux-linux-arm64`,"linux-x64":`@agimon-ai/doompi-runner-rmux-linux-x64`};var u=class{paths;clientPromise;runs=new Map;completions=new Map;constructor(e){this.paths=e}async launch(e){let i=await this.client();if(!i)return;this.paths.ensureDirectories();let a=`${n.SESSION_PREFIX}${e.id}`,s=this.paths.logPathFor(e.id),c=this.auxPaths(e.id,e.sessionId),l=!1,u=!1;try{this.prepareFiles(e,s,c)
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../runnerConfig/index.cjs"),n=require("../../constants/rmuxBackend.cjs"),r=require("../runnerSupervisor/index.cjs");let i=require("node:path");i=e.__toESM(i,1);let a=require("node:crypto"),o=require("node:fs");o=e.__toESM(o,1);let s=require("node:module"),c=require("@rmux/sdk");const l={"darwin-arm64":`@agimon-ai/doompi-runner-rmux-darwin-arm64`,"darwin-x64":`@agimon-ai/doompi-runner-rmux-darwin-x64`,"linux-arm64":`@agimon-ai/doompi-runner-rmux-linux-arm64`,"linux-x64":`@agimon-ai/doompi-runner-rmux-linux-x64`};var u=class{paths;clientPromise;runs=new Map;completions=new Map;constructor(e){this.paths=e}async launch(e){let i=await this.client();if(!i)return;this.paths.ensureDirectories();let a=`${n.SESSION_PREFIX}${e.id}`,s=this.paths.logPathFor(e.id),c=this.auxPaths(e.id,e.sessionId),l=!1,u=!1;try{this.prepareFiles(e,s,c);let d=await i.cmd(`new-session`,`-d`,`-P`,`-F`,n.PANE_PID_FORMAT,`-s`,a,`-c`,e.cwd,process.execPath,r.runtimeEntry(`runnerHost`),c.spec,c.gate,c.exit,{check:!0});l=!0,await i.cmd(`set-option`,`-t`,a,`remain-on-exit`,`on`,{check:!0}),await i.cmd(`pipe-pane`,`-t`,a,this.logPipeCommand(s,c.logDone,e),{check:!0});let f=i.session(a).pane(0,0),p=_(String(d.stdout??``))??await g(i,a);o.default.writeFileSync(c.gate,``,{mode:384}),u=!0;let m=!0,h=this.monitor(i,f,a,c);this.trackCompletion(a,h);let v={id:e.id,name:e.name,pid:p,logPath:s,backend:`rmux`,backendTarget:a,output:()=>m?T(s,t.getResultMaxBytes()*2):``,completion:()=>h,detach:()=>{m=!1},stop:()=>this.stop(a,p)};return e.interactive&&this.runs.set(e.name,this.interactiveRun(v,f,h)),h.finally(()=>this.runs.delete(e.name)).catch(e=>process.emitWarning(`RMUX completion failed for ${a}: ${D(e)}`)),v}catch(t){if(u)throw t;l&&await i.session(a).kill().catch(e=>process.emitWarning(`Could not clean up failed RMUX launch ${a}: ${D(e)}`)),r.cleanupSupervisorFiles(c),process.emitWarning(e.interactive?`RMUX launch unavailable for interactive runner: ${D(t)}`:`RMUX launch unavailable, using supervised subprocess: ${D(t)}`);return}}async stop(e,t){if(!n.OWNED_TARGET_PATTERN.test(e)||!Number.isInteger(t)||t<=0)return!1;let r=await this.client();if(!r)return!1;try{if(await C(r,e))return!0;let n=await g(r,e);if(n!==t)return!1;let i=w(n,e);return i===`gone`?!0:i===`failed`?!1:await x(r,e,3e3)||await C(r,e)?!0:await g(r,e)===t&&b(r,e)}catch(t){return process.emitWarning(`Could not stop RMUX target ${e}: ${D(t)}`),!1}}async watch(e,t,n){let r=await this.client();if(!r)return;let i=r.session(t).pane(0,0);try{await r.displayMessage(`#{pane_id}`,{target:t})}catch(e){process.emitWarning(`Could not watch RMUX target ${t}: ${D(e)}`);return}let a=this.monitor(r,i,t,this.auxPaths(e,n));return this.trackCompletion(t,a),a}async input(e,t){let n=await this.client();return n?await n.session(e).pane(0,0).sendText(t).catch(t=>{process.emitWarning(`Could not send input to RMUX target ${e}: ${D(t)}`)})!==void 0:!1}async capture(e){let t=await this.client();if(t)return t.session(e).pane(0,0).captureText().catch(t=>{process.emitWarning(`Could not capture RMUX target ${e}: ${D(t)}`)})}get(e){return this.runs.get(e)}readOutcome(e,t){return r.readExitMetadata(this.auxPaths(e,t).exit)}trackCompletion(e,t){this.completions.set(e,t),t.finally(()=>this.completions.delete(e)).catch(t=>process.emitWarning(`RMUX monitor failed for ${e}: ${D(t)}`))}client(){return this.clientPromise??=this.resolveClient(),this.clientPromise}async resolveClient(){let e=[p(),`rmux`].filter(e=>e!==void 0);for(let t of e)try{t!==`rmux`&&o.default.chmodSync(t,493);let e=new c.RMUX({binary:t,socketName:m(this.paths.repositoryPath())});return await e.capabilities(),e}catch(e){process.emitWarning(`RMUX candidate ${t} is unavailable: ${D(e)}`)}}prepareFiles(e,t,n){r.cleanupSupervisorFiles(n),o.default.writeFileSync(t,``,{mode:384});let i=h(e.sessionId,e.interactive),a={command:e.command,cwd:e.cwd,env:i};r.writeCommandSpec(n.spec,a)}auxPaths(e,t){return r.supervisorPaths(this.paths.stateDirectory(t),e)}logPipeCommand(e,n,a){let o=i.default.basename(e,`.log`);return r.shellJoin([process.execPath,r.runtimeEntry(`logSink`),e,this.paths.rotatedLogPathFor(o),String(t.getLogMaxBytes()),n,...a.interactive?[this.paths.rawLogPathFor(o)]:[]])}async monitor(e,t,i,a){try{let t=await v(e,i,a.exit);return await y(e,i,a.logDone),r.readExitMetadata(a.exit)??{code:t,signal:null}}finally{let o,s=async()=>{await C(e,i)||await t.server.session(i).kill()};try{await Promise.race([s().catch(e=>process.emitWarning(`Could not clean up completed RMUX target ${i}: ${D(e)}`)),new Promise(e=>{o=setTimeout(()=>{process.emitWarning(`Timed out cleaning up RMUX target ${i} after ${n.STOP_CLOSE_TIMEOUT_MS}ms`),e()},n.STOP_CLOSE_TIMEOUT_MS)})])}finally{o&&clearTimeout(o),r.cleanupSupervisorFiles(a)}}}interactiveRun(e,t,n){let r=``,i=!0,a=new Set;return(async()=>{for(;i;){let n=await t.captureText().catch(t=>(process.emitWarning(`Could not capture RMUX screen for ${e.name}: ${D(t)}`),r));if(n!==r){r=n;for(let e of a)e(n)}await E(100)}})(),n.finally(()=>{i=!1}).catch(t=>process.emitWarning(`RMUX interactive completion failed for ${e.name}: ${D(t)}`)),{...e,write:e=>{t.sendText(e).catch(e=>process.emitWarning(D(e)))},screen:()=>r,onData:e=>(a.add(e),()=>a.delete(e)),resize:(e,n)=>{t.resize({width:e,height:n}).catch(e=>process.emitWarning(D(e)))}}}};function d(e,t){let n=`${e}-${t}`,r=l[n];return r||process.emitWarning(`Bundled RMUX binary is unavailable for unsupported target ${n}`),r}function f(e=require("url").pathToFileURL(__filename).href){let t=process.env[n.RMUX_BINARY_ENV];if(t!==void 0&&t!==``)return t;let r=d(process.platform,process.arch);if(r)try{let t=(0,s.createRequire)(e).resolve(`${r}/package.json`);return i.default.join(i.default.dirname(t),`vendor`,`bin`,`rmux`)}catch(e){process.emitWarning(`Bundled RMUX binary is unavailable for ${r}: ${D(e)}`);return}}function p(){return f()}function m(e){let t=(0,a.createHash)(`sha256`).update(e).digest(`hex`).slice(0,12);return`${n.SESSION_PREFIX}${t}`}function h(e,t){let i={...process.env,...t?{}:n.NON_INTERACTIVE_ENV,...r.NO_TERMINAL_INPUT_ENV,PI_SESSION_ID:e};return t||delete i.FORCE_COLOR,Object.fromEntries(Object.entries(i).filter(e=>e[1]!==void 0))}async function g(e,t){let r=await e.displayMessage(n.PANE_PID_FORMAT,{target:t}),i=_(typeof r.message==`string`?r.message:``);if(i===void 0)throw Error(`RMUX did not report a pane pid for ${t}`);return i}function _(e){let t=Number(e.trim());return Number.isInteger(t)&&t>0?t:void 0}async function v(e,t,n){let i=r.watchForFile(n),a=!0,o=async()=>{for(;a;)if(await i.appeared(100))return null;return null},s=async()=>{for(;a;){if(await E(100),!a)return null;let n=await S(e,t);if(n.exited)return n.code}return null};try{return await Promise.race([o(),s()])}finally{a=!1,i.close()}}async function y(e,t,i){let a=Date.now()+n.LOG_DRAIN_TIMEOUT_MS,o=r.watchForFile(i),s=!0,c=async()=>{for(;s&&Date.now()<a;)if(await o.appeared(25))return},l=async()=>{for(;s&&Date.now()<a;)if(await E(25),!s||await C(e,t))return};try{await Promise.race([c(),l()])}finally{s=!1,o.close()}}async function b(e,t){return new Promise(r=>{let i=!1,a=e=>{i||(i=!0,clearTimeout(o),r(e))},o=setTimeout(()=>{process.emitWarning(`Timed out closing RMUX target ${t} after ${n.STOP_CLOSE_TIMEOUT_MS}ms`),a(!1)},n.STOP_CLOSE_TIMEOUT_MS);e.session(t).kill().then(()=>a(!0),e=>{process.emitWarning(`Could not close RMUX target ${t}: ${D(e)}`),a(!1)})})}async function x(e,t,n){let r=Date.now()+n;for(;Date.now()<r;){try{if((await S(e,t)).exited)return!0}catch(e){return process.emitWarning(`Could not verify RMUX target ${t} stopped: ${D(e)}`),!1}await E(100)}return!1}async function S(e,t){let r;try{let i=await e.displayMessage(n.PANE_STATE_FORMAT,{target:t});r=typeof i.message==`string`?i.message:``}catch{return{exited:!0,code:null}}let[i,a,...o]=r.trim().split(`:`);if(o.join(`:`)===``)return{exited:!0,code:null};if(i===`1`||i===`true`){let e=Number(a);return{exited:!0,code:Number.isInteger(e)?e:null}}return{exited:!1,code:null}}async function C(e,t){try{return(await e.cmd(`has-session`,`-t`,t)).returnCode!==0}catch{return!0}}function w(e,t){try{return process.kill(e,`SIGTERM`),`signalled`}catch(e){return e.code===`ESRCH`?`gone`:(process.emitWarning(`Could not signal RMUX target ${t}: ${D(e)}`),`failed`)}}function T(e,t){let n;try{let r=o.default.statSync(e).size,i=Math.min(r,t),a=Buffer.alloc(i);return n=o.default.openSync(e,`r`),o.default.readSync(n,a,0,i,r-i),a.toString(`utf8`)}catch(t){return t.code!==`ENOENT`&&process.emitWarning(`Could not read RMUX log ${e}: ${D(t)}`),``}finally{n!==void 0&&o.default.closeSync(n)}}function E(e){return new Promise(t=>{setTimeout(t,e).unref?.()})}function D(e){return e instanceof Error?e.message:String(e)}exports.RmuxBackend=u,exports.resolveBundledRmuxBinary=f,exports.rmuxPackageForTarget=d;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["SESSION_PREFIX","runtimeEntry","getResultMaxBytes","OWNED_TARGET_PATTERN","readExitMetadata","RMUX","supervisorPaths","path","shellJoin","getLogMaxBytes","RMUX_BINARY_ENV","createRequire","createHash","NON_INTERACTIVE_ENV","NO_TERMINAL_INPUT_ENV","fs","LOG_DRAIN_TIMEOUT_MS","STOP_CLOSE_TIMEOUT_MS","PANE_STATE_FORMAT"],"sources":["../../../src/services/rmuxBackend/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\n\nimport { type Pane, RMUX, type Rmux } from '@rmux/sdk';\n\nimport {\n EXECUTABLE_MODE,\n LOG_DRAIN_POLL_MS,\n LOG_DRAIN_TIMEOUT_MS,\n NON_INTERACTIVE_ENV,\n OWNED_TARGET_PATTERN,\n PANE_STATE_FORMAT,\n POLL_MS,\n RMUX_BINARY_ENV,\n SESSION_PREFIX,\n SOCKET_HASH_LENGTH,\n STOP_CLOSE_TIMEOUT_MS,\n STOP_GRACE_MS,\n} from '../../constants/rmuxBackend';\nimport type { RunHandle } from '../../types/launcher';\nimport type { PtyRun } from '../../types/ptyHost';\nimport type { IRmuxBackend, RmuxLaunchRequest } from '../../types/rmuxBackend';\nimport type { ExitResult } from '../../types/spawner';\nimport { getLogMaxBytes, getResultMaxBytes } from '../runnerConfig';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport {\n cleanupSupervisorFiles,\n type CommandSpec,\n NO_TERMINAL_INPUT_ENV,\n readExitMetadata,\n runtimeEntry,\n shellJoin,\n type SupervisorPaths,\n supervisorPaths,\n writeCommandSpec,\n} from '../runnerSupervisor';\n\n/** Pane liveness, its exit status, and whether the session still exists, in one read. */\nconst PACKAGE_BY_PLATFORM: Readonly<Record<string, string>> = {\n 'darwin-arm64': '@agimon-ai/doompi-runner-rmux-darwin-arm64',\n 'darwin-x64': '@agimon-ai/doompi-runner-rmux-darwin-x64',\n 'linux-arm64': '@agimon-ai/doompi-runner-rmux-linux-arm64',\n 'linux-x64': '@agimon-ai/doompi-runner-rmux-linux-x64',\n};\n\nexport class RmuxBackend implements IRmuxBackend {\n private clientPromise: Promise<Rmux | undefined> | undefined;\n private readonly runs = new Map<string, PtyRun>();\n private readonly completions = new Map<string, Promise<ExitResult>>();\n\n constructor(private readonly paths: IRunnerPaths) {}\n\n async launch(request: RmuxLaunchRequest): Promise<RunHandle | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n\n this.paths.ensureDirectories();\n const target = `${SESSION_PREFIX}${request.id}`;\n const logPath = this.paths.logPathFor(request.id);\n const aux = this.auxPaths(request.id, request.sessionId);\n let created = false;\n let released = false;\n\n try {\n this.prepareFiles(request, logPath, aux);\n await rmux.cmd(\n 'new-session',\n '-d',\n '-s',\n target,\n '-c',\n request.cwd,\n process.execPath,\n runtimeEntry('runnerHost'),\n aux.spec,\n aux.gate,\n aux.exit,\n { check: true },\n );\n created = true;\n // No remain-on-exit: a finished pane ends its session, and the last session\n // ending stops the server, so a completed run leaves no process behind.\n await rmux.cmd('pipe-pane', '-t', target, this.logPipeCommand(logPath, aux.logDone, request), { check: true });\n\n const pane = rmux.session(target).pane(0, 0);\n const pid = await panePid(rmux, target);\n fs.writeFileSync(aux.gate, '', { mode: 0o600 });\n released = true;\n\n let buffering = true;\n const completion = this.monitor(rmux, pane, target, aux);\n this.trackCompletion(target, completion);\n const stop = (): Promise<boolean> => this.stop(target, pid);\n const handle: RunHandle = {\n id: request.id,\n name: request.name,\n pid,\n logPath,\n backend: 'rmux',\n backendTarget: target,\n output: () => (buffering ? readTail(logPath, getResultMaxBytes() * 2) : ''),\n completion: () => completion,\n detach: () => {\n buffering = false;\n },\n stop,\n };\n\n if (request.interactive) this.runs.set(request.name, this.interactiveRun(handle, pane, completion));\n void completion\n .finally(() => this.runs.delete(request.name))\n .catch((error: unknown) => process.emitWarning(`RMUX completion failed for ${target}: ${errorMessage(error)}`));\n return handle;\n } catch (error) {\n if (released) throw error;\n if (created) {\n await rmux\n .session(target)\n .kill()\n .catch((cleanupError: unknown) =>\n process.emitWarning(`Could not clean up failed RMUX launch ${target}: ${errorMessage(cleanupError)}`),\n );\n }\n cleanupSupervisorFiles(aux);\n process.emitWarning(\n request.interactive\n ? `RMUX launch unavailable for interactive runner: ${errorMessage(error)}`\n : `RMUX launch unavailable, using supervised subprocess: ${errorMessage(error)}`,\n );\n return undefined;\n }\n }\n\n async stop(target: string, expectedPid: number): Promise<boolean> {\n if (!OWNED_TARGET_PATTERN.test(target) || !Number.isInteger(expectedPid) || expectedPid <= 0) return false;\n\n const rmux = await this.client();\n if (!rmux) return false;\n try {\n // A session that already ended took its process group with it.\n if (await sessionMissing(rmux, target)) return true;\n\n const pid = await panePid(rmux, target);\n if (pid !== expectedPid) return false;\n const signalled = signalPane(pid, target);\n if (signalled === 'gone') return true;\n if (signalled === 'failed') return false;\n\n if (await waitForPaneStop(rmux, target, STOP_GRACE_MS)) return true;\n if (await sessionMissing(rmux, target)) return true;\n\n // Revalidate after the grace period so a replacement pane is never closed.\n const currentPid = await panePid(rmux, target);\n if (currentPid !== expectedPid) return false;\n\n return closeOwnedSession(rmux, target);\n } catch (error) {\n process.emitWarning(`Could not stop RMUX target ${target}: ${errorMessage(error)}`);\n return false;\n }\n }\n\n async watch(id: string, target: string, sessionId?: string): Promise<ExitResult | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n const pane = rmux.session(target).pane(0, 0);\n try {\n await rmux.displayMessage('#{pane_id}', { target });\n } catch (error) {\n process.emitWarning(`Could not watch RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n }\n const completion = this.monitor(rmux, pane, target, this.auxPaths(id, sessionId));\n this.trackCompletion(target, completion);\n return completion;\n }\n\n async input(target: string, text: string): Promise<boolean> {\n const rmux = await this.client();\n if (!rmux) return false;\n const result = await rmux\n .session(target)\n .pane(0, 0)\n .sendText(text)\n .catch((error: unknown) => {\n process.emitWarning(`Could not send input to RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n return result !== undefined;\n }\n\n async capture(target: string): Promise<string | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n return rmux\n .session(target)\n .pane(0, 0)\n .captureText()\n .catch((error: unknown) => {\n process.emitWarning(`Could not capture RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n }\n\n get(name: string): PtyRun | undefined {\n return this.runs.get(name);\n }\n\n readOutcome(id: string, sessionId: string): ExitResult | undefined {\n return readExitMetadata(this.auxPaths(id, sessionId).exit);\n }\n\n private trackCompletion(target: string, completion: Promise<ExitResult>): void {\n this.completions.set(target, completion);\n void completion\n .finally(() => this.completions.delete(target))\n .catch((error: unknown) => process.emitWarning(`RMUX monitor failed for ${target}: ${errorMessage(error)}`));\n }\n\n private client(): Promise<Rmux | undefined> {\n this.clientPromise ??= this.resolveClient();\n return this.clientPromise;\n }\n\n private async resolveClient(): Promise<Rmux | undefined> {\n const binaries = [bundledBinary(), 'rmux'].filter((value): value is string => value !== undefined);\n for (const binary of binaries) {\n try {\n if (binary !== 'rmux') fs.chmodSync(binary, EXECUTABLE_MODE);\n const client = new RMUX({ binary, socketName: socketName(this.paths.repositoryPath()) });\n await client.capabilities();\n return client;\n } catch (error) {\n process.emitWarning(`RMUX candidate ${binary} is unavailable: ${errorMessage(error)}`);\n // Try the next compatible candidate. Non-interactive calls can use the subprocess launcher.\n }\n }\n return undefined;\n }\n\n private prepareFiles(request: RmuxLaunchRequest, logPath: string, aux: SupervisorPaths): void {\n cleanupSupervisorFiles(aux);\n fs.writeFileSync(logPath, '', { mode: 0o600 });\n const env = environment(request.sessionId, request.interactive);\n // runnerHost supervises the command and watches the lifeline itself, so an\n // owner that dies mid-run is handled inside the pane rather than around it.\n const spec: CommandSpec = { command: request.command, cwd: request.cwd, env };\n writeCommandSpec(aux.spec, spec);\n }\n\n private auxPaths(id: string, sessionId?: string): SupervisorPaths {\n return supervisorPaths(this.paths.stateDirectory(sessionId), id);\n }\n\n /**\n * The sink the pane pipes into.\n *\n * An interactive run also asks for a raw copy: the scrubbed log it writes\n * beside it has had cursor movement taken out, which is what a terminal\n * needs most. Only interactive runs pay for it, because only they can be\n * attached to.\n */\n private logPipeCommand(logPath: string, donePath: string, request: RmuxLaunchRequest): string {\n const id = path.basename(logPath, '.log');\n return shellJoin([\n process.execPath,\n runtimeEntry('logSink'),\n logPath,\n this.paths.rotatedLogPathFor(id),\n String(getLogMaxBytes()),\n donePath,\n ...(request.interactive ? [this.paths.rawLogPathFor(id)] : []),\n ]);\n }\n\n private async monitor(rmux: Rmux, pane: Pane, target: string, aux: SupervisorPaths): Promise<ExitResult> {\n try {\n const paneCode = await waitForRunEnd(rmux, target, aux.exit);\n await waitForLogDrain(rmux, target, aux.logDone);\n return readExitMetadata(aux.exit) ?? { code: paneCode, signal: null };\n } finally {\n // The pane ends its own session, so this only covers a session that somehow\n // outlived its pane. Killing unconditionally would warn on every clean run.\n if (!(await sessionMissing(rmux, target).catch(() => false))) {\n await pane.server\n .session(target)\n .kill()\n .catch((error: unknown) =>\n process.emitWarning(`Could not clean up completed RMUX target ${target}: ${errorMessage(error)}`),\n );\n }\n cleanupSupervisorFiles(aux);\n }\n }\n\n private interactiveRun(handle: RunHandle, pane: Pane, completion: Promise<ExitResult>): PtyRun {\n let screen = '';\n let active = true;\n const handlers = new Set<(data: string) => void>();\n const refresh = async (): Promise<void> => {\n while (active) {\n const next = await pane.captureText().catch((error: unknown) => {\n process.emitWarning(`Could not capture RMUX screen for ${handle.name}: ${errorMessage(error)}`);\n return screen;\n });\n if (next !== screen) {\n screen = next;\n for (const handler of handlers) handler(next);\n }\n await delay(POLL_MS);\n }\n };\n void refresh();\n void completion\n .finally(() => {\n active = false;\n })\n .catch((error: unknown) =>\n process.emitWarning(`RMUX interactive completion failed for ${handle.name}: ${errorMessage(error)}`),\n );\n return {\n ...handle,\n write: (text) => {\n void pane.sendText(text).catch((error: unknown) => process.emitWarning(errorMessage(error)));\n },\n screen: () => screen,\n onData: (handler) => {\n handlers.add(handler);\n return () => handlers.delete(handler);\n },\n resize: (cols, rows) => {\n void pane\n .resize({ width: cols, height: rows })\n .catch((error: unknown) => process.emitWarning(errorMessage(error)));\n },\n };\n }\n}\n\nexport function rmuxPackageForTarget(platform: string, architecture: string): string | undefined {\n const target = `${platform}-${architecture}`;\n const packageName = PACKAGE_BY_PLATFORM[target];\n if (!packageName) process.emitWarning(`Bundled RMUX binary is unavailable for unsupported target ${target}`);\n return packageName;\n}\n\nexport function resolveBundledRmuxBinary(moduleUrl = import.meta.url): string | undefined {\n const configured = process.env[RMUX_BINARY_ENV];\n if (configured !== undefined && configured !== '') return configured;\n const packageName = rmuxPackageForTarget(process.platform, process.arch);\n if (!packageName) return undefined;\n try {\n const require = createRequire(moduleUrl);\n const manifest = require.resolve(`${packageName}/package.json`);\n return path.join(path.dirname(manifest), 'vendor', 'bin', 'rmux');\n } catch (error) {\n process.emitWarning(`Bundled RMUX binary is unavailable for ${packageName}: ${errorMessage(error)}`);\n return undefined;\n }\n}\n\nfunction bundledBinary(): string | undefined {\n return resolveBundledRmuxBinary();\n}\n\nfunction socketName(repositoryPath: string): string {\n const hash = createHash('sha256').update(repositoryPath).digest('hex').slice(0, SOCKET_HASH_LENGTH);\n return `${SESSION_PREFIX}${hash}`;\n}\n\nfunction environment(sessionId: string, interactive: boolean): Record<string, string> {\n const values: NodeJS.ProcessEnv = {\n ...process.env,\n ...(interactive ? {} : NON_INTERACTIVE_ENV),\n // Applied to interactive runs as well: a pane is a tty either way, and an\n // agent sending input to a prompt still has no way past a pager.\n ...NO_TERMINAL_INPUT_ENV,\n PI_SESSION_ID: sessionId,\n };\n if (!interactive) delete values['FORCE_COLOR'];\n return Object.fromEntries(\n Object.entries(values).filter((entry): entry is [string, string] => entry[1] !== undefined),\n );\n}\n\nasync function panePid(rmux: Rmux, target: string): Promise<number> {\n const response = await rmux.displayMessage('#{pane_pid}', { target });\n const pid = Number(response['message']);\n if (!Number.isInteger(pid) || pid <= 0) throw new Error(`RMUX did not report a pane pid for ${target}`);\n return pid;\n}\n\n/**\n * Waits for the run to end, preferring the supervisor's own record of it.\n *\n * The sidecar is written before the command's pane can end, and it outlives the\n * server that the pane's exit takes down with it. Asking the server is the\n * fallback for a supervisor that died without recording anything.\n */\nasync function waitForRunEnd(rmux: Rmux, target: string, exitPath: string): Promise<number | null> {\n for (;;) {\n if (fs.existsSync(exitPath)) return null;\n const state = await readPaneExit(rmux, target);\n if (state.exited) return state.code;\n await delay(POLL_MS);\n }\n}\n\n/**\n * Gives the log sink a moment to mark the log complete.\n *\n * The sink dies with the session it was piped from, so once the session is gone\n * the marker is never coming and there is nothing left to wait for. Its writes\n * are synchronous, so the log itself is already on disk either way.\n */\nasync function waitForLogDrain(rmux: Rmux, target: string, donePath: string): Promise<void> {\n const deadline = Date.now() + LOG_DRAIN_TIMEOUT_MS;\n while (Date.now() < deadline) {\n if (fs.existsSync(donePath)) return;\n if (await sessionMissing(rmux, target)) return;\n await delay(LOG_DRAIN_POLL_MS);\n }\n}\n\nasync function closeOwnedSession(rmux: Rmux, target: string): Promise<boolean> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (result: boolean): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(result);\n };\n const timer = setTimeout(() => {\n process.emitWarning(`Timed out closing RMUX target ${target} after ${STOP_CLOSE_TIMEOUT_MS}ms`);\n finish(false);\n }, STOP_CLOSE_TIMEOUT_MS);\n void rmux\n .session(target)\n .kill()\n .then(\n () => finish(true),\n (error: unknown) => {\n process.emitWarning(`Could not close RMUX target ${target}: ${errorMessage(error)}`);\n finish(false);\n },\n );\n });\n}\n\nasync function waitForPaneStop(rmux: Rmux, target: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n try {\n if ((await readPaneExit(rmux, target)).exited) return true;\n } catch (error) {\n process.emitWarning(`Could not verify RMUX target ${target} stopped: ${errorMessage(error)}`);\n return false;\n }\n await delay(POLL_MS);\n }\n return false;\n}\n\n/**\n * One formatted read answers both questions this poll asks, so watching a run\n * costs the same single rmux call it always did.\n */\nasync function readPaneExit(rmux: Rmux, target: string): Promise<{ exited: boolean; code: number | null }> {\n let message: string;\n try {\n const response = await rmux.displayMessage(PANE_STATE_FORMAT, { target });\n message = typeof response['message'] === 'string' ? response['message'] : '';\n } catch {\n // The last session ending stops the server, so a server that cannot be\n // reached is the ordinary end of the last run rather than a fault.\n return { exited: true, code: null };\n }\n const [dead, status, ...rest] = message.trim().split(':');\n // A missing session formats as a live pane with no name rather than failing,\n // so the name is what says whether there is still anything to watch.\n if (rest.join(':') === '') return { exited: true, code: null };\n if (dead === '1' || dead === 'true') {\n const code = Number(status);\n return { exited: true, code: Number.isInteger(code) ? code : null };\n }\n return { exited: false, code: null };\n}\n\n/** A session that cannot be found, on a server that may itself be gone. */\nasync function sessionMissing(rmux: Rmux, target: string): Promise<boolean> {\n try {\n const probe = await rmux.cmd('has-session', '-t', target);\n return probe.returnCode !== 0;\n } catch {\n // No server to ask means no session to find.\n return true;\n }\n}\n\n/**\n * A pane that was already gone and a signal that could not be delivered are\n * opposite answers to \"is it stopped\", so they are reported apart.\n */\nfunction signalPane(pid: number, target: string): 'signalled' | 'gone' | 'failed' {\n try {\n process.kill(pid, 'SIGTERM');\n return 'signalled';\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ESRCH') return 'gone';\n process.emitWarning(`Could not signal RMUX target ${target}: ${errorMessage(error)}`);\n return 'failed';\n }\n}\n\nfunction readTail(target: string, maxBytes: number): string {\n let handle: number | undefined;\n try {\n const size = fs.statSync(target).size;\n const bytes = Math.min(size, maxBytes);\n const buffer = Buffer.alloc(bytes);\n handle = fs.openSync(target, 'r');\n fs.readSync(handle, buffer, 0, bytes, size - bytes);\n return buffer.toString('utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n process.emitWarning(`Could not read RMUX log ${target}: ${errorMessage(error)}`);\n }\n return '';\n } finally {\n if (handle !== undefined) fs.closeSync(handle);\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n timer.unref?.();\n });\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["SESSION_PREFIX","PANE_PID_FORMAT","runtimeEntry","getResultMaxBytes","OWNED_TARGET_PATTERN","readExitMetadata","RMUX","supervisorPaths","path","shellJoin","getLogMaxBytes","STOP_CLOSE_TIMEOUT_MS","RMUX_BINARY_ENV","createRequire","createHash","NON_INTERACTIVE_ENV","NO_TERMINAL_INPUT_ENV","watchForFile","LOG_DRAIN_TIMEOUT_MS","PANE_STATE_FORMAT","fs"],"sources":["../../../src/services/rmuxBackend/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\n\nimport { type Pane, RMUX, type Rmux } from '@rmux/sdk';\n\nimport {\n EXECUTABLE_MODE,\n LOG_DRAIN_POLL_MS,\n LOG_DRAIN_TIMEOUT_MS,\n NON_INTERACTIVE_ENV,\n OWNED_TARGET_PATTERN,\n PANE_PID_FORMAT,\n PANE_STATE_FORMAT,\n POLL_MS,\n RMUX_BINARY_ENV,\n SESSION_PREFIX,\n SOCKET_HASH_LENGTH,\n STOP_CLOSE_TIMEOUT_MS,\n STOP_GRACE_MS,\n} from '../../constants/rmuxBackend';\nimport type { RunHandle } from '../../types/launcher';\nimport type { PtyRun } from '../../types/ptyHost';\nimport type { IRmuxBackend, RmuxLaunchRequest } from '../../types/rmuxBackend';\nimport type { ExitResult } from '../../types/spawner';\nimport { getLogMaxBytes, getResultMaxBytes } from '../runnerConfig';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport {\n cleanupSupervisorFiles,\n type CommandSpec,\n NO_TERMINAL_INPUT_ENV,\n readExitMetadata,\n runtimeEntry,\n shellJoin,\n type SupervisorPaths,\n supervisorPaths,\n watchForFile,\n writeCommandSpec,\n} from '../runnerSupervisor';\n\n/** Pane liveness, its exit status, and whether the session still exists, in one read. */\nconst PACKAGE_BY_PLATFORM: Readonly<Record<string, string>> = {\n 'darwin-arm64': '@agimon-ai/doompi-runner-rmux-darwin-arm64',\n 'darwin-x64': '@agimon-ai/doompi-runner-rmux-darwin-x64',\n 'linux-arm64': '@agimon-ai/doompi-runner-rmux-linux-arm64',\n 'linux-x64': '@agimon-ai/doompi-runner-rmux-linux-x64',\n};\n\nexport class RmuxBackend implements IRmuxBackend {\n private clientPromise: Promise<Rmux | undefined> | undefined;\n private readonly runs = new Map<string, PtyRun>();\n private readonly completions = new Map<string, Promise<ExitResult>>();\n\n constructor(private readonly paths: IRunnerPaths) {}\n\n async launch(request: RmuxLaunchRequest): Promise<RunHandle | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n\n this.paths.ensureDirectories();\n const target = `${SESSION_PREFIX}${request.id}`;\n const logPath = this.paths.logPathFor(request.id);\n const aux = this.auxPaths(request.id, request.sessionId);\n let created = false;\n let released = false;\n\n try {\n this.prepareFiles(request, logPath, aux);\n // `-P -F` reports the pane pid from the call that creates the pane, so the\n // launch does not spend a second round trip asking for it.\n const opened = await rmux.cmd(\n 'new-session',\n '-d',\n '-P',\n '-F',\n PANE_PID_FORMAT,\n '-s',\n target,\n '-c',\n request.cwd,\n process.execPath,\n runtimeEntry('runnerHost'),\n aux.spec,\n aux.gate,\n aux.exit,\n { check: true },\n );\n created = true;\n // Keep the pane until the log reader drains. Otherwise a fast shell can\n // end the session before the reader starts, losing its final output.\n await rmux.cmd('set-option', '-t', target, 'remain-on-exit', 'on', { check: true });\n await rmux.cmd('pipe-pane', '-t', target, this.logPipeCommand(logPath, aux.logDone, request), { check: true });\n\n const pane = rmux.session(target).pane(0, 0);\n const pid = reportedPid(String(opened.stdout ?? '')) ?? (await panePid(rmux, target));\n fs.writeFileSync(aux.gate, '', { mode: 0o600 });\n released = true;\n\n let buffering = true;\n const completion = this.monitor(rmux, pane, target, aux);\n this.trackCompletion(target, completion);\n const stop = (): Promise<boolean> => this.stop(target, pid);\n const handle: RunHandle = {\n id: request.id,\n name: request.name,\n pid,\n logPath,\n backend: 'rmux',\n backendTarget: target,\n output: () => (buffering ? readTail(logPath, getResultMaxBytes() * 2) : ''),\n completion: () => completion,\n detach: () => {\n buffering = false;\n },\n stop,\n };\n\n if (request.interactive) this.runs.set(request.name, this.interactiveRun(handle, pane, completion));\n void completion\n .finally(() => this.runs.delete(request.name))\n .catch((error: unknown) => process.emitWarning(`RMUX completion failed for ${target}: ${errorMessage(error)}`));\n return handle;\n } catch (error) {\n if (released) throw error;\n if (created) {\n await rmux\n .session(target)\n .kill()\n .catch((cleanupError: unknown) =>\n process.emitWarning(`Could not clean up failed RMUX launch ${target}: ${errorMessage(cleanupError)}`),\n );\n }\n cleanupSupervisorFiles(aux);\n process.emitWarning(\n request.interactive\n ? `RMUX launch unavailable for interactive runner: ${errorMessage(error)}`\n : `RMUX launch unavailable, using supervised subprocess: ${errorMessage(error)}`,\n );\n return undefined;\n }\n }\n\n async stop(target: string, expectedPid: number): Promise<boolean> {\n if (!OWNED_TARGET_PATTERN.test(target) || !Number.isInteger(expectedPid) || expectedPid <= 0) return false;\n\n const rmux = await this.client();\n if (!rmux) return false;\n try {\n // A session that already ended took its process group with it.\n if (await sessionMissing(rmux, target)) return true;\n\n const pid = await panePid(rmux, target);\n if (pid !== expectedPid) return false;\n const signalled = signalPane(pid, target);\n if (signalled === 'gone') return true;\n if (signalled === 'failed') return false;\n\n if (await waitForPaneStop(rmux, target, STOP_GRACE_MS)) return true;\n if (await sessionMissing(rmux, target)) return true;\n\n // Revalidate after the grace period so a replacement pane is never closed.\n const currentPid = await panePid(rmux, target);\n if (currentPid !== expectedPid) return false;\n\n return closeOwnedSession(rmux, target);\n } catch (error) {\n process.emitWarning(`Could not stop RMUX target ${target}: ${errorMessage(error)}`);\n return false;\n }\n }\n\n async watch(id: string, target: string, sessionId?: string): Promise<ExitResult | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n const pane = rmux.session(target).pane(0, 0);\n try {\n await rmux.displayMessage('#{pane_id}', { target });\n } catch (error) {\n process.emitWarning(`Could not watch RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n }\n const completion = this.monitor(rmux, pane, target, this.auxPaths(id, sessionId));\n this.trackCompletion(target, completion);\n return completion;\n }\n\n async input(target: string, text: string): Promise<boolean> {\n const rmux = await this.client();\n if (!rmux) return false;\n const result = await rmux\n .session(target)\n .pane(0, 0)\n .sendText(text)\n .catch((error: unknown) => {\n process.emitWarning(`Could not send input to RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n return result !== undefined;\n }\n\n async capture(target: string): Promise<string | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n return rmux\n .session(target)\n .pane(0, 0)\n .captureText()\n .catch((error: unknown) => {\n process.emitWarning(`Could not capture RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n }\n\n get(name: string): PtyRun | undefined {\n return this.runs.get(name);\n }\n\n readOutcome(id: string, sessionId: string): ExitResult | undefined {\n return readExitMetadata(this.auxPaths(id, sessionId).exit);\n }\n\n private trackCompletion(target: string, completion: Promise<ExitResult>): void {\n this.completions.set(target, completion);\n void completion\n .finally(() => this.completions.delete(target))\n .catch((error: unknown) => process.emitWarning(`RMUX monitor failed for ${target}: ${errorMessage(error)}`));\n }\n\n private client(): Promise<Rmux | undefined> {\n this.clientPromise ??= this.resolveClient();\n return this.clientPromise;\n }\n\n private async resolveClient(): Promise<Rmux | undefined> {\n const binaries = [bundledBinary(), 'rmux'].filter((value): value is string => value !== undefined);\n for (const binary of binaries) {\n try {\n if (binary !== 'rmux') fs.chmodSync(binary, EXECUTABLE_MODE);\n const client = new RMUX({ binary, socketName: socketName(this.paths.repositoryPath()) });\n await client.capabilities();\n return client;\n } catch (error) {\n process.emitWarning(`RMUX candidate ${binary} is unavailable: ${errorMessage(error)}`);\n // Try the next compatible candidate. Non-interactive calls can use the subprocess launcher.\n }\n }\n return undefined;\n }\n\n private prepareFiles(request: RmuxLaunchRequest, logPath: string, aux: SupervisorPaths): void {\n cleanupSupervisorFiles(aux);\n fs.writeFileSync(logPath, '', { mode: 0o600 });\n const env = environment(request.sessionId, request.interactive);\n // runnerHost supervises the command and watches the lifeline itself, so an\n // owner that dies mid-run is handled inside the pane rather than around it.\n const spec: CommandSpec = { command: request.command, cwd: request.cwd, env };\n writeCommandSpec(aux.spec, spec);\n }\n\n private auxPaths(id: string, sessionId?: string): SupervisorPaths {\n return supervisorPaths(this.paths.stateDirectory(sessionId), id);\n }\n\n /**\n * The sink the pane pipes into.\n *\n * An interactive run also asks for a raw copy: the scrubbed log it writes\n * beside it has had cursor movement taken out, which is what a terminal\n * needs most. Only interactive runs pay for it, because only they can be\n * attached to.\n */\n private logPipeCommand(logPath: string, donePath: string, request: RmuxLaunchRequest): string {\n const id = path.basename(logPath, '.log');\n return shellJoin([\n process.execPath,\n runtimeEntry('logSink'),\n logPath,\n this.paths.rotatedLogPathFor(id),\n String(getLogMaxBytes()),\n donePath,\n ...(request.interactive ? [this.paths.rawLogPathFor(id)] : []),\n ]);\n }\n\n private async monitor(rmux: Rmux, pane: Pane, target: string, aux: SupervisorPaths): Promise<ExitResult> {\n try {\n const paneCode = await waitForRunEnd(rmux, target, aux.exit);\n await waitForLogDrain(rmux, target, aux.logDone);\n return readExitMetadata(aux.exit) ?? { code: paneCode, signal: null };\n } finally {\n // The pane stays alive for log capture. Remove its session after draining,\n // allowing the last completed run to shut down the mux server too.\n let timer: ReturnType<typeof setTimeout> | undefined;\n const cleanup = async (): Promise<void> => {\n if (!(await sessionMissing(rmux, target))) await pane.server.session(target).kill();\n };\n try {\n await Promise.race([\n cleanup().catch((error: unknown) =>\n process.emitWarning(`Could not clean up completed RMUX target ${target}: ${errorMessage(error)}`),\n ),\n new Promise<void>((resolve) => {\n timer = setTimeout(() => {\n process.emitWarning(`Timed out cleaning up RMUX target ${target} after ${STOP_CLOSE_TIMEOUT_MS}ms`);\n resolve();\n }, STOP_CLOSE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n cleanupSupervisorFiles(aux);\n }\n }\n }\n\n private interactiveRun(handle: RunHandle, pane: Pane, completion: Promise<ExitResult>): PtyRun {\n let screen = '';\n let active = true;\n const handlers = new Set<(data: string) => void>();\n const refresh = async (): Promise<void> => {\n while (active) {\n const next = await pane.captureText().catch((error: unknown) => {\n process.emitWarning(`Could not capture RMUX screen for ${handle.name}: ${errorMessage(error)}`);\n return screen;\n });\n if (next !== screen) {\n screen = next;\n for (const handler of handlers) handler(next);\n }\n await delay(POLL_MS);\n }\n };\n void refresh();\n void completion\n .finally(() => {\n active = false;\n })\n .catch((error: unknown) =>\n process.emitWarning(`RMUX interactive completion failed for ${handle.name}: ${errorMessage(error)}`),\n );\n return {\n ...handle,\n write: (text) => {\n void pane.sendText(text).catch((error: unknown) => process.emitWarning(errorMessage(error)));\n },\n screen: () => screen,\n onData: (handler) => {\n handlers.add(handler);\n return () => handlers.delete(handler);\n },\n resize: (cols, rows) => {\n void pane\n .resize({ width: cols, height: rows })\n .catch((error: unknown) => process.emitWarning(errorMessage(error)));\n },\n };\n }\n}\n\nexport function rmuxPackageForTarget(platform: string, architecture: string): string | undefined {\n const target = `${platform}-${architecture}`;\n const packageName = PACKAGE_BY_PLATFORM[target];\n if (!packageName) process.emitWarning(`Bundled RMUX binary is unavailable for unsupported target ${target}`);\n return packageName;\n}\n\nexport function resolveBundledRmuxBinary(moduleUrl = import.meta.url): string | undefined {\n const configured = process.env[RMUX_BINARY_ENV];\n if (configured !== undefined && configured !== '') return configured;\n const packageName = rmuxPackageForTarget(process.platform, process.arch);\n if (!packageName) return undefined;\n try {\n const require = createRequire(moduleUrl);\n const manifest = require.resolve(`${packageName}/package.json`);\n return path.join(path.dirname(manifest), 'vendor', 'bin', 'rmux');\n } catch (error) {\n process.emitWarning(`Bundled RMUX binary is unavailable for ${packageName}: ${errorMessage(error)}`);\n return undefined;\n }\n}\n\nfunction bundledBinary(): string | undefined {\n return resolveBundledRmuxBinary();\n}\n\nfunction socketName(repositoryPath: string): string {\n const hash = createHash('sha256').update(repositoryPath).digest('hex').slice(0, SOCKET_HASH_LENGTH);\n return `${SESSION_PREFIX}${hash}`;\n}\n\nfunction environment(sessionId: string, interactive: boolean): Record<string, string> {\n const values: NodeJS.ProcessEnv = {\n ...process.env,\n ...(interactive ? {} : NON_INTERACTIVE_ENV),\n // Applied to interactive runs as well: a pane is a tty either way, and an\n // agent sending input to a prompt still has no way past a pager.\n ...NO_TERMINAL_INPUT_ENV,\n PI_SESSION_ID: sessionId,\n };\n if (!interactive) delete values['FORCE_COLOR'];\n return Object.fromEntries(\n Object.entries(values).filter((entry): entry is [string, string] => entry[1] !== undefined),\n );\n}\n\nasync function panePid(rmux: Rmux, target: string): Promise<number> {\n const response = await rmux.displayMessage(PANE_PID_FORMAT, { target });\n const message = typeof response['message'] === 'string' ? response['message'] : '';\n const pid = reportedPid(message);\n if (pid === undefined) throw new Error(`RMUX did not report a pane pid for ${target}`);\n return pid;\n}\n\n/** A pane pid as rmux prints it, or nothing when the output holds no usable pid. */\nfunction reportedPid(output: string): number | undefined {\n const pid = Number(output.trim());\n return Number.isInteger(pid) && pid > 0 ? pid : undefined;\n}\n\n/**\n * Waits for the run to end, preferring the supervisor's own record of it.\n *\n * The sidecar is written before the command's pane can end, and it outlives the\n * server that the pane's exit takes down with it. Watching for it means a run\n * that finishes inside a poll interval is observed when it finishes rather than\n * on the next tick. Asking the server stays the fallback for a supervisor that\n * died without recording anything.\n */\nasync function waitForRunEnd(rmux: Rmux, target: string, exitPath: string): Promise<number | null> {\n const landed = watchForFile(exitPath);\n let active = true;\n const readEvidence = async (): Promise<null> => {\n while (active) {\n if (await landed.appeared(POLL_MS)) return null;\n }\n return null;\n };\n const probePane = async (): Promise<number | null> => {\n while (active) {\n await delay(POLL_MS);\n if (!active) return null;\n const state = await readPaneExit(rmux, target);\n if (state.exited) return state.code;\n }\n return null;\n };\n try {\n // A stalled SDK request must not block the supervisor's durable evidence.\n // Keep only one pane request in flight, even when it never answers.\n return await Promise.race([readEvidence(), probePane()]);\n } finally {\n active = false;\n landed.close();\n }\n}\n\n/**\n * Gives the log sink a moment to mark the log complete.\n *\n * Normal completion retains the session until the sink records its final\n * writes. A lost session cannot guarantee a later marker, so it ends the wait.\n */\nasync function waitForLogDrain(rmux: Rmux, target: string, donePath: string): Promise<void> {\n const deadline = Date.now() + LOG_DRAIN_TIMEOUT_MS;\n const landed = watchForFile(donePath);\n let active = true;\n const readEvidence = async (): Promise<void> => {\n while (active && Date.now() < deadline) {\n if (await landed.appeared(LOG_DRAIN_POLL_MS)) return;\n }\n };\n const probeSession = async (): Promise<void> => {\n while (active && Date.now() < deadline) {\n await delay(LOG_DRAIN_POLL_MS);\n if (!active) return;\n if (await sessionMissing(rmux, target)) return;\n }\n };\n try {\n // The file loop enforces the deadline while a session query is outstanding.\n await Promise.race([readEvidence(), probeSession()]);\n } finally {\n active = false;\n landed.close();\n }\n}\n\nasync function closeOwnedSession(rmux: Rmux, target: string): Promise<boolean> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (result: boolean): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(result);\n };\n const timer = setTimeout(() => {\n process.emitWarning(`Timed out closing RMUX target ${target} after ${STOP_CLOSE_TIMEOUT_MS}ms`);\n finish(false);\n }, STOP_CLOSE_TIMEOUT_MS);\n void rmux\n .session(target)\n .kill()\n .then(\n () => finish(true),\n (error: unknown) => {\n process.emitWarning(`Could not close RMUX target ${target}: ${errorMessage(error)}`);\n finish(false);\n },\n );\n });\n}\n\nasync function waitForPaneStop(rmux: Rmux, target: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n try {\n if ((await readPaneExit(rmux, target)).exited) return true;\n } catch (error) {\n process.emitWarning(`Could not verify RMUX target ${target} stopped: ${errorMessage(error)}`);\n return false;\n }\n await delay(POLL_MS);\n }\n return false;\n}\n\n/**\n * One formatted read answers both questions this poll asks, so watching a run\n * costs the same single rmux call it always did.\n */\nasync function readPaneExit(rmux: Rmux, target: string): Promise<{ exited: boolean; code: number | null }> {\n let message: string;\n try {\n const response = await rmux.displayMessage(PANE_STATE_FORMAT, { target });\n message = typeof response['message'] === 'string' ? response['message'] : '';\n } catch {\n // The last session ending stops the server, so a server that cannot be\n // reached is the ordinary end of the last run rather than a fault.\n return { exited: true, code: null };\n }\n const [dead, status, ...rest] = message.trim().split(':');\n // A missing session formats as a live pane with no name rather than failing,\n // so the name is what says whether there is still anything to watch.\n if (rest.join(':') === '') return { exited: true, code: null };\n if (dead === '1' || dead === 'true') {\n const code = Number(status);\n return { exited: true, code: Number.isInteger(code) ? code : null };\n }\n return { exited: false, code: null };\n}\n\n/** A session that cannot be found, on a server that may itself be gone. */\nasync function sessionMissing(rmux: Rmux, target: string): Promise<boolean> {\n try {\n const probe = await rmux.cmd('has-session', '-t', target);\n return probe.returnCode !== 0;\n } catch {\n // No server to ask means no session to find.\n return true;\n }\n}\n\n/**\n * A pane that was already gone and a signal that could not be delivered are\n * opposite answers to \"is it stopped\", so they are reported apart.\n */\nfunction signalPane(pid: number, target: string): 'signalled' | 'gone' | 'failed' {\n try {\n process.kill(pid, 'SIGTERM');\n return 'signalled';\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ESRCH') return 'gone';\n process.emitWarning(`Could not signal RMUX target ${target}: ${errorMessage(error)}`);\n return 'failed';\n }\n}\n\nfunction readTail(target: string, maxBytes: number): string {\n let handle: number | undefined;\n try {\n const size = fs.statSync(target).size;\n const bytes = Math.min(size, maxBytes);\n const buffer = Buffer.alloc(bytes);\n handle = fs.openSync(target, 'r');\n fs.readSync(handle, buffer, 0, bytes, size - bytes);\n return buffer.toString('utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n process.emitWarning(`Could not read RMUX log ${target}: ${errorMessage(error)}`);\n }\n return '';\n } finally {\n if (handle !== undefined) fs.closeSync(handle);\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n timer.unref?.();\n });\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/services/rmuxBackend/index.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/services/rmuxBackend/index.ts"],"mappings":";;;;;;qBAiDa,uBAAuB;mBAKL;UAJrB;mBACS;mBACA;EAEjB,YAA6B,OAAO;EAE9B,OAAO,SAAS,oBAAoB,QAAQ;EAuF5C,KAAK,gBAAgB,sBAAsB;EA6B3C,MAAM,YAAY,gBAAgB,qBAAqB,QAAQ;EAe/D,MAAM,gBAAgB,eAAe;EAcrC,QAAQ,iBAAiB;EAa/B,IAAI,eAAe;EAInB,YAAY,YAAY,oBAAoB;UAIpC;UAOA;UAKM;UAgBN;UAUA;;;;;;;;;UAYA;UAaM;UA+BN;;wBA4CM,qBAAqB,kBAAkB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/rmuxBackend/index.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/rmuxBackend/index.ts"],"mappings":";;;;;;qBAiDa,uBAAuB;mBAKL;UAJrB;mBACS;mBACA;EAEjB,YAA6B,OAAO;EAE9B,OAAO,SAAS,oBAAoB,QAAQ;EAuF5C,KAAK,gBAAgB,sBAAsB;EA6B3C,MAAM,YAAY,gBAAgB,qBAAqB,QAAQ;EAe/D,MAAM,gBAAgB,eAAe;EAcrC,QAAQ,iBAAiB;EAa/B,IAAI,eAAe;EAInB,YAAY,YAAY,oBAAoB;UAIpC;UAOA;UAKM;UAgBN;UAUA;;;;;;;;;UAYA;UAaM;UA+BN;;wBA4CM,qBAAqB,kBAAkB"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{getLogMaxBytes as e,getResultMaxBytes as t}from"../runnerConfig/index.mjs";import{LOG_DRAIN_TIMEOUT_MS as n,NON_INTERACTIVE_ENV as r,OWNED_TARGET_PATTERN as i,
|
|
1
|
+
import{getLogMaxBytes as e,getResultMaxBytes as t}from"../runnerConfig/index.mjs";import{LOG_DRAIN_TIMEOUT_MS as n,NON_INTERACTIVE_ENV as r,OWNED_TARGET_PATTERN as i,PANE_PID_FORMAT as a,PANE_STATE_FORMAT as o,RMUX_BINARY_ENV as s,SESSION_PREFIX as c,STOP_CLOSE_TIMEOUT_MS as l}from"../../constants/rmuxBackend.mjs";import{NO_TERMINAL_INPUT_ENV as u,cleanupSupervisorFiles as d,readExitMetadata as f,runtimeEntry as p,shellJoin as m,supervisorPaths as h,watchForFile as g,writeCommandSpec as _}from"../runnerSupervisor/index.mjs";import{createRequire as v}from"node:module";import y from"node:path";import{createHash as b}from"node:crypto";import x from"node:fs";import{RMUX as S}from"@rmux/sdk";const C={"darwin-arm64":`@agimon-ai/doompi-runner-rmux-darwin-arm64`,"darwin-x64":`@agimon-ai/doompi-runner-rmux-darwin-x64`,"linux-arm64":`@agimon-ai/doompi-runner-rmux-linux-arm64`,"linux-x64":`@agimon-ai/doompi-runner-rmux-linux-x64`};var w=class{paths;clientPromise;runs=new Map;completions=new Map;constructor(e){this.paths=e}async launch(e){let n=await this.client();if(!n)return;this.paths.ensureDirectories();let r=`${c}${e.id}`,i=this.paths.logPathFor(e.id),o=this.auxPaths(e.id,e.sessionId),s=!1,l=!1;try{this.prepareFiles(e,i,o);let c=await n.cmd(`new-session`,`-d`,`-P`,`-F`,a,`-s`,r,`-c`,e.cwd,process.execPath,p(`runnerHost`),o.spec,o.gate,o.exit,{check:!0});s=!0,await n.cmd(`set-option`,`-t`,r,`remain-on-exit`,`on`,{check:!0}),await n.cmd(`pipe-pane`,`-t`,r,this.logPipeCommand(i,o.logDone,e),{check:!0});let u=n.session(r).pane(0,0),d=j(String(c.stdout??``))??await A(n,r);x.writeFileSync(o.gate,``,{mode:384}),l=!0;let f=!0,m=this.monitor(n,u,r,o);this.trackCompletion(r,m);let h={id:e.id,name:e.name,pid:d,logPath:i,backend:`rmux`,backendTarget:r,output:()=>f?z(i,t()*2):``,completion:()=>m,detach:()=>{f=!1},stop:()=>this.stop(r,d)};return e.interactive&&this.runs.set(e.name,this.interactiveRun(h,u,m)),m.finally(()=>this.runs.delete(e.name)).catch(e=>process.emitWarning(`RMUX completion failed for ${r}: ${V(e)}`)),h}catch(t){if(l)throw t;s&&await n.session(r).kill().catch(e=>process.emitWarning(`Could not clean up failed RMUX launch ${r}: ${V(e)}`)),d(o),process.emitWarning(e.interactive?`RMUX launch unavailable for interactive runner: ${V(t)}`:`RMUX launch unavailable, using supervised subprocess: ${V(t)}`);return}}async stop(e,t){if(!i.test(e)||!Number.isInteger(t)||t<=0)return!1;let n=await this.client();if(!n)return!1;try{if(await L(n,e))return!0;let r=await A(n,e);if(r!==t)return!1;let i=R(r,e);return i===`gone`?!0:i===`failed`?!1:await F(n,e,3e3)||await L(n,e)?!0:await A(n,e)===t&&P(n,e)}catch(t){return process.emitWarning(`Could not stop RMUX target ${e}: ${V(t)}`),!1}}async watch(e,t,n){let r=await this.client();if(!r)return;let i=r.session(t).pane(0,0);try{await r.displayMessage(`#{pane_id}`,{target:t})}catch(e){process.emitWarning(`Could not watch RMUX target ${t}: ${V(e)}`);return}let a=this.monitor(r,i,t,this.auxPaths(e,n));return this.trackCompletion(t,a),a}async input(e,t){let n=await this.client();return n?await n.session(e).pane(0,0).sendText(t).catch(t=>{process.emitWarning(`Could not send input to RMUX target ${e}: ${V(t)}`)})!==void 0:!1}async capture(e){let t=await this.client();if(t)return t.session(e).pane(0,0).captureText().catch(t=>{process.emitWarning(`Could not capture RMUX target ${e}: ${V(t)}`)})}get(e){return this.runs.get(e)}readOutcome(e,t){return f(this.auxPaths(e,t).exit)}trackCompletion(e,t){this.completions.set(e,t),t.finally(()=>this.completions.delete(e)).catch(t=>process.emitWarning(`RMUX monitor failed for ${e}: ${V(t)}`))}client(){return this.clientPromise??=this.resolveClient(),this.clientPromise}async resolveClient(){let e=[D(),`rmux`].filter(e=>e!==void 0);for(let t of e)try{t!==`rmux`&&x.chmodSync(t,493);let e=new S({binary:t,socketName:O(this.paths.repositoryPath())});return await e.capabilities(),e}catch(e){process.emitWarning(`RMUX candidate ${t} is unavailable: ${V(e)}`)}}prepareFiles(e,t,n){d(n),x.writeFileSync(t,``,{mode:384});let r=k(e.sessionId,e.interactive),i={command:e.command,cwd:e.cwd,env:r};_(n.spec,i)}auxPaths(e,t){return h(this.paths.stateDirectory(t),e)}logPipeCommand(t,n,r){let i=y.basename(t,`.log`);return m([process.execPath,p(`logSink`),t,this.paths.rotatedLogPathFor(i),String(e()),n,...r.interactive?[this.paths.rawLogPathFor(i)]:[]])}async monitor(e,t,n,r){try{let t=await M(e,n,r.exit);return await N(e,n,r.logDone),f(r.exit)??{code:t,signal:null}}finally{let i,a=async()=>{await L(e,n)||await t.server.session(n).kill()};try{await Promise.race([a().catch(e=>process.emitWarning(`Could not clean up completed RMUX target ${n}: ${V(e)}`)),new Promise(e=>{i=setTimeout(()=>{process.emitWarning(`Timed out cleaning up RMUX target ${n} after ${l}ms`),e()},l)})])}finally{i&&clearTimeout(i),d(r)}}}interactiveRun(e,t,n){let r=``,i=!0,a=new Set;return(async()=>{for(;i;){let n=await t.captureText().catch(t=>(process.emitWarning(`Could not capture RMUX screen for ${e.name}: ${V(t)}`),r));if(n!==r){r=n;for(let e of a)e(n)}await B(100)}})(),n.finally(()=>{i=!1}).catch(t=>process.emitWarning(`RMUX interactive completion failed for ${e.name}: ${V(t)}`)),{...e,write:e=>{t.sendText(e).catch(e=>process.emitWarning(V(e)))},screen:()=>r,onData:e=>(a.add(e),()=>a.delete(e)),resize:(e,n)=>{t.resize({width:e,height:n}).catch(e=>process.emitWarning(V(e)))}}}};function T(e,t){let n=`${e}-${t}`,r=C[n];return r||process.emitWarning(`Bundled RMUX binary is unavailable for unsupported target ${n}`),r}function E(e=import.meta.url){let t=process.env[s];if(t!==void 0&&t!==``)return t;let n=T(process.platform,process.arch);if(n)try{let t=v(e).resolve(`${n}/package.json`);return y.join(y.dirname(t),`vendor`,`bin`,`rmux`)}catch(e){process.emitWarning(`Bundled RMUX binary is unavailable for ${n}: ${V(e)}`);return}}function D(){return E()}function O(e){let t=b(`sha256`).update(e).digest(`hex`).slice(0,12);return`${c}${t}`}function k(e,t){let n={...process.env,...t?{}:r,...u,PI_SESSION_ID:e};return t||delete n.FORCE_COLOR,Object.fromEntries(Object.entries(n).filter(e=>e[1]!==void 0))}async function A(e,t){let n=await e.displayMessage(a,{target:t}),r=j(typeof n.message==`string`?n.message:``);if(r===void 0)throw Error(`RMUX did not report a pane pid for ${t}`);return r}function j(e){let t=Number(e.trim());return Number.isInteger(t)&&t>0?t:void 0}async function M(e,t,n){let r=g(n),i=!0,a=async()=>{for(;i;)if(await r.appeared(100))return null;return null},o=async()=>{for(;i;){if(await B(100),!i)return null;let n=await I(e,t);if(n.exited)return n.code}return null};try{return await Promise.race([a(),o()])}finally{i=!1,r.close()}}async function N(e,t,r){let i=Date.now()+n,a=g(r),o=!0,s=async()=>{for(;o&&Date.now()<i;)if(await a.appeared(25))return},c=async()=>{for(;o&&Date.now()<i;)if(await B(25),!o||await L(e,t))return};try{await Promise.race([s(),c()])}finally{o=!1,a.close()}}async function P(e,t){return new Promise(n=>{let r=!1,i=e=>{r||(r=!0,clearTimeout(a),n(e))},a=setTimeout(()=>{process.emitWarning(`Timed out closing RMUX target ${t} after ${l}ms`),i(!1)},l);e.session(t).kill().then(()=>i(!0),e=>{process.emitWarning(`Could not close RMUX target ${t}: ${V(e)}`),i(!1)})})}async function F(e,t,n){let r=Date.now()+n;for(;Date.now()<r;){try{if((await I(e,t)).exited)return!0}catch(e){return process.emitWarning(`Could not verify RMUX target ${t} stopped: ${V(e)}`),!1}await B(100)}return!1}async function I(e,t){let n;try{let r=await e.displayMessage(o,{target:t});n=typeof r.message==`string`?r.message:``}catch{return{exited:!0,code:null}}let[r,i,...a]=n.trim().split(`:`);if(a.join(`:`)===``)return{exited:!0,code:null};if(r===`1`||r===`true`){let e=Number(i);return{exited:!0,code:Number.isInteger(e)?e:null}}return{exited:!1,code:null}}async function L(e,t){try{return(await e.cmd(`has-session`,`-t`,t)).returnCode!==0}catch{return!0}}function R(e,t){try{return process.kill(e,`SIGTERM`),`signalled`}catch(e){return e.code===`ESRCH`?`gone`:(process.emitWarning(`Could not signal RMUX target ${t}: ${V(e)}`),`failed`)}}function z(e,t){let n;try{let r=x.statSync(e).size,i=Math.min(r,t),a=Buffer.alloc(i);return n=x.openSync(e,`r`),x.readSync(n,a,0,i,r-i),a.toString(`utf8`)}catch(t){return t.code!==`ENOENT`&&process.emitWarning(`Could not read RMUX log ${e}: ${V(t)}`),``}finally{n!==void 0&&x.closeSync(n)}}function B(e){return new Promise(t=>{setTimeout(t,e).unref?.()})}function V(e){return e instanceof Error?e.message:String(e)}export{w as RmuxBackend,E as resolveBundledRmuxBinary,T as rmuxPackageForTarget};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/rmuxBackend/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\n\nimport { type Pane, RMUX, type Rmux } from '@rmux/sdk';\n\nimport {\n EXECUTABLE_MODE,\n LOG_DRAIN_POLL_MS,\n LOG_DRAIN_TIMEOUT_MS,\n NON_INTERACTIVE_ENV,\n OWNED_TARGET_PATTERN,\n PANE_STATE_FORMAT,\n POLL_MS,\n RMUX_BINARY_ENV,\n SESSION_PREFIX,\n SOCKET_HASH_LENGTH,\n STOP_CLOSE_TIMEOUT_MS,\n STOP_GRACE_MS,\n} from '../../constants/rmuxBackend';\nimport type { RunHandle } from '../../types/launcher';\nimport type { PtyRun } from '../../types/ptyHost';\nimport type { IRmuxBackend, RmuxLaunchRequest } from '../../types/rmuxBackend';\nimport type { ExitResult } from '../../types/spawner';\nimport { getLogMaxBytes, getResultMaxBytes } from '../runnerConfig';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport {\n cleanupSupervisorFiles,\n type CommandSpec,\n NO_TERMINAL_INPUT_ENV,\n readExitMetadata,\n runtimeEntry,\n shellJoin,\n type SupervisorPaths,\n supervisorPaths,\n writeCommandSpec,\n} from '../runnerSupervisor';\n\n/** Pane liveness, its exit status, and whether the session still exists, in one read. */\nconst PACKAGE_BY_PLATFORM: Readonly<Record<string, string>> = {\n 'darwin-arm64': '@agimon-ai/doompi-runner-rmux-darwin-arm64',\n 'darwin-x64': '@agimon-ai/doompi-runner-rmux-darwin-x64',\n 'linux-arm64': '@agimon-ai/doompi-runner-rmux-linux-arm64',\n 'linux-x64': '@agimon-ai/doompi-runner-rmux-linux-x64',\n};\n\nexport class RmuxBackend implements IRmuxBackend {\n private clientPromise: Promise<Rmux | undefined> | undefined;\n private readonly runs = new Map<string, PtyRun>();\n private readonly completions = new Map<string, Promise<ExitResult>>();\n\n constructor(private readonly paths: IRunnerPaths) {}\n\n async launch(request: RmuxLaunchRequest): Promise<RunHandle | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n\n this.paths.ensureDirectories();\n const target = `${SESSION_PREFIX}${request.id}`;\n const logPath = this.paths.logPathFor(request.id);\n const aux = this.auxPaths(request.id, request.sessionId);\n let created = false;\n let released = false;\n\n try {\n this.prepareFiles(request, logPath, aux);\n await rmux.cmd(\n 'new-session',\n '-d',\n '-s',\n target,\n '-c',\n request.cwd,\n process.execPath,\n runtimeEntry('runnerHost'),\n aux.spec,\n aux.gate,\n aux.exit,\n { check: true },\n );\n created = true;\n // No remain-on-exit: a finished pane ends its session, and the last session\n // ending stops the server, so a completed run leaves no process behind.\n await rmux.cmd('pipe-pane', '-t', target, this.logPipeCommand(logPath, aux.logDone, request), { check: true });\n\n const pane = rmux.session(target).pane(0, 0);\n const pid = await panePid(rmux, target);\n fs.writeFileSync(aux.gate, '', { mode: 0o600 });\n released = true;\n\n let buffering = true;\n const completion = this.monitor(rmux, pane, target, aux);\n this.trackCompletion(target, completion);\n const stop = (): Promise<boolean> => this.stop(target, pid);\n const handle: RunHandle = {\n id: request.id,\n name: request.name,\n pid,\n logPath,\n backend: 'rmux',\n backendTarget: target,\n output: () => (buffering ? readTail(logPath, getResultMaxBytes() * 2) : ''),\n completion: () => completion,\n detach: () => {\n buffering = false;\n },\n stop,\n };\n\n if (request.interactive) this.runs.set(request.name, this.interactiveRun(handle, pane, completion));\n void completion\n .finally(() => this.runs.delete(request.name))\n .catch((error: unknown) => process.emitWarning(`RMUX completion failed for ${target}: ${errorMessage(error)}`));\n return handle;\n } catch (error) {\n if (released) throw error;\n if (created) {\n await rmux\n .session(target)\n .kill()\n .catch((cleanupError: unknown) =>\n process.emitWarning(`Could not clean up failed RMUX launch ${target}: ${errorMessage(cleanupError)}`),\n );\n }\n cleanupSupervisorFiles(aux);\n process.emitWarning(\n request.interactive\n ? `RMUX launch unavailable for interactive runner: ${errorMessage(error)}`\n : `RMUX launch unavailable, using supervised subprocess: ${errorMessage(error)}`,\n );\n return undefined;\n }\n }\n\n async stop(target: string, expectedPid: number): Promise<boolean> {\n if (!OWNED_TARGET_PATTERN.test(target) || !Number.isInteger(expectedPid) || expectedPid <= 0) return false;\n\n const rmux = await this.client();\n if (!rmux) return false;\n try {\n // A session that already ended took its process group with it.\n if (await sessionMissing(rmux, target)) return true;\n\n const pid = await panePid(rmux, target);\n if (pid !== expectedPid) return false;\n const signalled = signalPane(pid, target);\n if (signalled === 'gone') return true;\n if (signalled === 'failed') return false;\n\n if (await waitForPaneStop(rmux, target, STOP_GRACE_MS)) return true;\n if (await sessionMissing(rmux, target)) return true;\n\n // Revalidate after the grace period so a replacement pane is never closed.\n const currentPid = await panePid(rmux, target);\n if (currentPid !== expectedPid) return false;\n\n return closeOwnedSession(rmux, target);\n } catch (error) {\n process.emitWarning(`Could not stop RMUX target ${target}: ${errorMessage(error)}`);\n return false;\n }\n }\n\n async watch(id: string, target: string, sessionId?: string): Promise<ExitResult | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n const pane = rmux.session(target).pane(0, 0);\n try {\n await rmux.displayMessage('#{pane_id}', { target });\n } catch (error) {\n process.emitWarning(`Could not watch RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n }\n const completion = this.monitor(rmux, pane, target, this.auxPaths(id, sessionId));\n this.trackCompletion(target, completion);\n return completion;\n }\n\n async input(target: string, text: string): Promise<boolean> {\n const rmux = await this.client();\n if (!rmux) return false;\n const result = await rmux\n .session(target)\n .pane(0, 0)\n .sendText(text)\n .catch((error: unknown) => {\n process.emitWarning(`Could not send input to RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n return result !== undefined;\n }\n\n async capture(target: string): Promise<string | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n return rmux\n .session(target)\n .pane(0, 0)\n .captureText()\n .catch((error: unknown) => {\n process.emitWarning(`Could not capture RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n }\n\n get(name: string): PtyRun | undefined {\n return this.runs.get(name);\n }\n\n readOutcome(id: string, sessionId: string): ExitResult | undefined {\n return readExitMetadata(this.auxPaths(id, sessionId).exit);\n }\n\n private trackCompletion(target: string, completion: Promise<ExitResult>): void {\n this.completions.set(target, completion);\n void completion\n .finally(() => this.completions.delete(target))\n .catch((error: unknown) => process.emitWarning(`RMUX monitor failed for ${target}: ${errorMessage(error)}`));\n }\n\n private client(): Promise<Rmux | undefined> {\n this.clientPromise ??= this.resolveClient();\n return this.clientPromise;\n }\n\n private async resolveClient(): Promise<Rmux | undefined> {\n const binaries = [bundledBinary(), 'rmux'].filter((value): value is string => value !== undefined);\n for (const binary of binaries) {\n try {\n if (binary !== 'rmux') fs.chmodSync(binary, EXECUTABLE_MODE);\n const client = new RMUX({ binary, socketName: socketName(this.paths.repositoryPath()) });\n await client.capabilities();\n return client;\n } catch (error) {\n process.emitWarning(`RMUX candidate ${binary} is unavailable: ${errorMessage(error)}`);\n // Try the next compatible candidate. Non-interactive calls can use the subprocess launcher.\n }\n }\n return undefined;\n }\n\n private prepareFiles(request: RmuxLaunchRequest, logPath: string, aux: SupervisorPaths): void {\n cleanupSupervisorFiles(aux);\n fs.writeFileSync(logPath, '', { mode: 0o600 });\n const env = environment(request.sessionId, request.interactive);\n // runnerHost supervises the command and watches the lifeline itself, so an\n // owner that dies mid-run is handled inside the pane rather than around it.\n const spec: CommandSpec = { command: request.command, cwd: request.cwd, env };\n writeCommandSpec(aux.spec, spec);\n }\n\n private auxPaths(id: string, sessionId?: string): SupervisorPaths {\n return supervisorPaths(this.paths.stateDirectory(sessionId), id);\n }\n\n /**\n * The sink the pane pipes into.\n *\n * An interactive run also asks for a raw copy: the scrubbed log it writes\n * beside it has had cursor movement taken out, which is what a terminal\n * needs most. Only interactive runs pay for it, because only they can be\n * attached to.\n */\n private logPipeCommand(logPath: string, donePath: string, request: RmuxLaunchRequest): string {\n const id = path.basename(logPath, '.log');\n return shellJoin([\n process.execPath,\n runtimeEntry('logSink'),\n logPath,\n this.paths.rotatedLogPathFor(id),\n String(getLogMaxBytes()),\n donePath,\n ...(request.interactive ? [this.paths.rawLogPathFor(id)] : []),\n ]);\n }\n\n private async monitor(rmux: Rmux, pane: Pane, target: string, aux: SupervisorPaths): Promise<ExitResult> {\n try {\n const paneCode = await waitForRunEnd(rmux, target, aux.exit);\n await waitForLogDrain(rmux, target, aux.logDone);\n return readExitMetadata(aux.exit) ?? { code: paneCode, signal: null };\n } finally {\n // The pane ends its own session, so this only covers a session that somehow\n // outlived its pane. Killing unconditionally would warn on every clean run.\n if (!(await sessionMissing(rmux, target).catch(() => false))) {\n await pane.server\n .session(target)\n .kill()\n .catch((error: unknown) =>\n process.emitWarning(`Could not clean up completed RMUX target ${target}: ${errorMessage(error)}`),\n );\n }\n cleanupSupervisorFiles(aux);\n }\n }\n\n private interactiveRun(handle: RunHandle, pane: Pane, completion: Promise<ExitResult>): PtyRun {\n let screen = '';\n let active = true;\n const handlers = new Set<(data: string) => void>();\n const refresh = async (): Promise<void> => {\n while (active) {\n const next = await pane.captureText().catch((error: unknown) => {\n process.emitWarning(`Could not capture RMUX screen for ${handle.name}: ${errorMessage(error)}`);\n return screen;\n });\n if (next !== screen) {\n screen = next;\n for (const handler of handlers) handler(next);\n }\n await delay(POLL_MS);\n }\n };\n void refresh();\n void completion\n .finally(() => {\n active = false;\n })\n .catch((error: unknown) =>\n process.emitWarning(`RMUX interactive completion failed for ${handle.name}: ${errorMessage(error)}`),\n );\n return {\n ...handle,\n write: (text) => {\n void pane.sendText(text).catch((error: unknown) => process.emitWarning(errorMessage(error)));\n },\n screen: () => screen,\n onData: (handler) => {\n handlers.add(handler);\n return () => handlers.delete(handler);\n },\n resize: (cols, rows) => {\n void pane\n .resize({ width: cols, height: rows })\n .catch((error: unknown) => process.emitWarning(errorMessage(error)));\n },\n };\n }\n}\n\nexport function rmuxPackageForTarget(platform: string, architecture: string): string | undefined {\n const target = `${platform}-${architecture}`;\n const packageName = PACKAGE_BY_PLATFORM[target];\n if (!packageName) process.emitWarning(`Bundled RMUX binary is unavailable for unsupported target ${target}`);\n return packageName;\n}\n\nexport function resolveBundledRmuxBinary(moduleUrl = import.meta.url): string | undefined {\n const configured = process.env[RMUX_BINARY_ENV];\n if (configured !== undefined && configured !== '') return configured;\n const packageName = rmuxPackageForTarget(process.platform, process.arch);\n if (!packageName) return undefined;\n try {\n const require = createRequire(moduleUrl);\n const manifest = require.resolve(`${packageName}/package.json`);\n return path.join(path.dirname(manifest), 'vendor', 'bin', 'rmux');\n } catch (error) {\n process.emitWarning(`Bundled RMUX binary is unavailable for ${packageName}: ${errorMessage(error)}`);\n return undefined;\n }\n}\n\nfunction bundledBinary(): string | undefined {\n return resolveBundledRmuxBinary();\n}\n\nfunction socketName(repositoryPath: string): string {\n const hash = createHash('sha256').update(repositoryPath).digest('hex').slice(0, SOCKET_HASH_LENGTH);\n return `${SESSION_PREFIX}${hash}`;\n}\n\nfunction environment(sessionId: string, interactive: boolean): Record<string, string> {\n const values: NodeJS.ProcessEnv = {\n ...process.env,\n ...(interactive ? {} : NON_INTERACTIVE_ENV),\n // Applied to interactive runs as well: a pane is a tty either way, and an\n // agent sending input to a prompt still has no way past a pager.\n ...NO_TERMINAL_INPUT_ENV,\n PI_SESSION_ID: sessionId,\n };\n if (!interactive) delete values['FORCE_COLOR'];\n return Object.fromEntries(\n Object.entries(values).filter((entry): entry is [string, string] => entry[1] !== undefined),\n );\n}\n\nasync function panePid(rmux: Rmux, target: string): Promise<number> {\n const response = await rmux.displayMessage('#{pane_pid}', { target });\n const pid = Number(response['message']);\n if (!Number.isInteger(pid) || pid <= 0) throw new Error(`RMUX did not report a pane pid for ${target}`);\n return pid;\n}\n\n/**\n * Waits for the run to end, preferring the supervisor's own record of it.\n *\n * The sidecar is written before the command's pane can end, and it outlives the\n * server that the pane's exit takes down with it. Asking the server is the\n * fallback for a supervisor that died without recording anything.\n */\nasync function waitForRunEnd(rmux: Rmux, target: string, exitPath: string): Promise<number | null> {\n for (;;) {\n if (fs.existsSync(exitPath)) return null;\n const state = await readPaneExit(rmux, target);\n if (state.exited) return state.code;\n await delay(POLL_MS);\n }\n}\n\n/**\n * Gives the log sink a moment to mark the log complete.\n *\n * The sink dies with the session it was piped from, so once the session is gone\n * the marker is never coming and there is nothing left to wait for. Its writes\n * are synchronous, so the log itself is already on disk either way.\n */\nasync function waitForLogDrain(rmux: Rmux, target: string, donePath: string): Promise<void> {\n const deadline = Date.now() + LOG_DRAIN_TIMEOUT_MS;\n while (Date.now() < deadline) {\n if (fs.existsSync(donePath)) return;\n if (await sessionMissing(rmux, target)) return;\n await delay(LOG_DRAIN_POLL_MS);\n }\n}\n\nasync function closeOwnedSession(rmux: Rmux, target: string): Promise<boolean> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (result: boolean): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(result);\n };\n const timer = setTimeout(() => {\n process.emitWarning(`Timed out closing RMUX target ${target} after ${STOP_CLOSE_TIMEOUT_MS}ms`);\n finish(false);\n }, STOP_CLOSE_TIMEOUT_MS);\n void rmux\n .session(target)\n .kill()\n .then(\n () => finish(true),\n (error: unknown) => {\n process.emitWarning(`Could not close RMUX target ${target}: ${errorMessage(error)}`);\n finish(false);\n },\n );\n });\n}\n\nasync function waitForPaneStop(rmux: Rmux, target: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n try {\n if ((await readPaneExit(rmux, target)).exited) return true;\n } catch (error) {\n process.emitWarning(`Could not verify RMUX target ${target} stopped: ${errorMessage(error)}`);\n return false;\n }\n await delay(POLL_MS);\n }\n return false;\n}\n\n/**\n * One formatted read answers both questions this poll asks, so watching a run\n * costs the same single rmux call it always did.\n */\nasync function readPaneExit(rmux: Rmux, target: string): Promise<{ exited: boolean; code: number | null }> {\n let message: string;\n try {\n const response = await rmux.displayMessage(PANE_STATE_FORMAT, { target });\n message = typeof response['message'] === 'string' ? response['message'] : '';\n } catch {\n // The last session ending stops the server, so a server that cannot be\n // reached is the ordinary end of the last run rather than a fault.\n return { exited: true, code: null };\n }\n const [dead, status, ...rest] = message.trim().split(':');\n // A missing session formats as a live pane with no name rather than failing,\n // so the name is what says whether there is still anything to watch.\n if (rest.join(':') === '') return { exited: true, code: null };\n if (dead === '1' || dead === 'true') {\n const code = Number(status);\n return { exited: true, code: Number.isInteger(code) ? code : null };\n }\n return { exited: false, code: null };\n}\n\n/** A session that cannot be found, on a server that may itself be gone. */\nasync function sessionMissing(rmux: Rmux, target: string): Promise<boolean> {\n try {\n const probe = await rmux.cmd('has-session', '-t', target);\n return probe.returnCode !== 0;\n } catch {\n // No server to ask means no session to find.\n return true;\n }\n}\n\n/**\n * A pane that was already gone and a signal that could not be delivered are\n * opposite answers to \"is it stopped\", so they are reported apart.\n */\nfunction signalPane(pid: number, target: string): 'signalled' | 'gone' | 'failed' {\n try {\n process.kill(pid, 'SIGTERM');\n return 'signalled';\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ESRCH') return 'gone';\n process.emitWarning(`Could not signal RMUX target ${target}: ${errorMessage(error)}`);\n return 'failed';\n }\n}\n\nfunction readTail(target: string, maxBytes: number): string {\n let handle: number | undefined;\n try {\n const size = fs.statSync(target).size;\n const bytes = Math.min(size, maxBytes);\n const buffer = Buffer.alloc(bytes);\n handle = fs.openSync(target, 'r');\n fs.readSync(handle, buffer, 0, bytes, size - bytes);\n return buffer.toString('utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n process.emitWarning(`Could not read RMUX log ${target}: ${errorMessage(error)}`);\n }\n return '';\n } finally {\n if (handle !== undefined) fs.closeSync(handle);\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n timer.unref?.();\n });\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/rmuxBackend/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\n\nimport { type Pane, RMUX, type Rmux } from '@rmux/sdk';\n\nimport {\n EXECUTABLE_MODE,\n LOG_DRAIN_POLL_MS,\n LOG_DRAIN_TIMEOUT_MS,\n NON_INTERACTIVE_ENV,\n OWNED_TARGET_PATTERN,\n PANE_PID_FORMAT,\n PANE_STATE_FORMAT,\n POLL_MS,\n RMUX_BINARY_ENV,\n SESSION_PREFIX,\n SOCKET_HASH_LENGTH,\n STOP_CLOSE_TIMEOUT_MS,\n STOP_GRACE_MS,\n} from '../../constants/rmuxBackend';\nimport type { RunHandle } from '../../types/launcher';\nimport type { PtyRun } from '../../types/ptyHost';\nimport type { IRmuxBackend, RmuxLaunchRequest } from '../../types/rmuxBackend';\nimport type { ExitResult } from '../../types/spawner';\nimport { getLogMaxBytes, getResultMaxBytes } from '../runnerConfig';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport {\n cleanupSupervisorFiles,\n type CommandSpec,\n NO_TERMINAL_INPUT_ENV,\n readExitMetadata,\n runtimeEntry,\n shellJoin,\n type SupervisorPaths,\n supervisorPaths,\n watchForFile,\n writeCommandSpec,\n} from '../runnerSupervisor';\n\n/** Pane liveness, its exit status, and whether the session still exists, in one read. */\nconst PACKAGE_BY_PLATFORM: Readonly<Record<string, string>> = {\n 'darwin-arm64': '@agimon-ai/doompi-runner-rmux-darwin-arm64',\n 'darwin-x64': '@agimon-ai/doompi-runner-rmux-darwin-x64',\n 'linux-arm64': '@agimon-ai/doompi-runner-rmux-linux-arm64',\n 'linux-x64': '@agimon-ai/doompi-runner-rmux-linux-x64',\n};\n\nexport class RmuxBackend implements IRmuxBackend {\n private clientPromise: Promise<Rmux | undefined> | undefined;\n private readonly runs = new Map<string, PtyRun>();\n private readonly completions = new Map<string, Promise<ExitResult>>();\n\n constructor(private readonly paths: IRunnerPaths) {}\n\n async launch(request: RmuxLaunchRequest): Promise<RunHandle | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n\n this.paths.ensureDirectories();\n const target = `${SESSION_PREFIX}${request.id}`;\n const logPath = this.paths.logPathFor(request.id);\n const aux = this.auxPaths(request.id, request.sessionId);\n let created = false;\n let released = false;\n\n try {\n this.prepareFiles(request, logPath, aux);\n // `-P -F` reports the pane pid from the call that creates the pane, so the\n // launch does not spend a second round trip asking for it.\n const opened = await rmux.cmd(\n 'new-session',\n '-d',\n '-P',\n '-F',\n PANE_PID_FORMAT,\n '-s',\n target,\n '-c',\n request.cwd,\n process.execPath,\n runtimeEntry('runnerHost'),\n aux.spec,\n aux.gate,\n aux.exit,\n { check: true },\n );\n created = true;\n // Keep the pane until the log reader drains. Otherwise a fast shell can\n // end the session before the reader starts, losing its final output.\n await rmux.cmd('set-option', '-t', target, 'remain-on-exit', 'on', { check: true });\n await rmux.cmd('pipe-pane', '-t', target, this.logPipeCommand(logPath, aux.logDone, request), { check: true });\n\n const pane = rmux.session(target).pane(0, 0);\n const pid = reportedPid(String(opened.stdout ?? '')) ?? (await panePid(rmux, target));\n fs.writeFileSync(aux.gate, '', { mode: 0o600 });\n released = true;\n\n let buffering = true;\n const completion = this.monitor(rmux, pane, target, aux);\n this.trackCompletion(target, completion);\n const stop = (): Promise<boolean> => this.stop(target, pid);\n const handle: RunHandle = {\n id: request.id,\n name: request.name,\n pid,\n logPath,\n backend: 'rmux',\n backendTarget: target,\n output: () => (buffering ? readTail(logPath, getResultMaxBytes() * 2) : ''),\n completion: () => completion,\n detach: () => {\n buffering = false;\n },\n stop,\n };\n\n if (request.interactive) this.runs.set(request.name, this.interactiveRun(handle, pane, completion));\n void completion\n .finally(() => this.runs.delete(request.name))\n .catch((error: unknown) => process.emitWarning(`RMUX completion failed for ${target}: ${errorMessage(error)}`));\n return handle;\n } catch (error) {\n if (released) throw error;\n if (created) {\n await rmux\n .session(target)\n .kill()\n .catch((cleanupError: unknown) =>\n process.emitWarning(`Could not clean up failed RMUX launch ${target}: ${errorMessage(cleanupError)}`),\n );\n }\n cleanupSupervisorFiles(aux);\n process.emitWarning(\n request.interactive\n ? `RMUX launch unavailable for interactive runner: ${errorMessage(error)}`\n : `RMUX launch unavailable, using supervised subprocess: ${errorMessage(error)}`,\n );\n return undefined;\n }\n }\n\n async stop(target: string, expectedPid: number): Promise<boolean> {\n if (!OWNED_TARGET_PATTERN.test(target) || !Number.isInteger(expectedPid) || expectedPid <= 0) return false;\n\n const rmux = await this.client();\n if (!rmux) return false;\n try {\n // A session that already ended took its process group with it.\n if (await sessionMissing(rmux, target)) return true;\n\n const pid = await panePid(rmux, target);\n if (pid !== expectedPid) return false;\n const signalled = signalPane(pid, target);\n if (signalled === 'gone') return true;\n if (signalled === 'failed') return false;\n\n if (await waitForPaneStop(rmux, target, STOP_GRACE_MS)) return true;\n if (await sessionMissing(rmux, target)) return true;\n\n // Revalidate after the grace period so a replacement pane is never closed.\n const currentPid = await panePid(rmux, target);\n if (currentPid !== expectedPid) return false;\n\n return closeOwnedSession(rmux, target);\n } catch (error) {\n process.emitWarning(`Could not stop RMUX target ${target}: ${errorMessage(error)}`);\n return false;\n }\n }\n\n async watch(id: string, target: string, sessionId?: string): Promise<ExitResult | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n const pane = rmux.session(target).pane(0, 0);\n try {\n await rmux.displayMessage('#{pane_id}', { target });\n } catch (error) {\n process.emitWarning(`Could not watch RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n }\n const completion = this.monitor(rmux, pane, target, this.auxPaths(id, sessionId));\n this.trackCompletion(target, completion);\n return completion;\n }\n\n async input(target: string, text: string): Promise<boolean> {\n const rmux = await this.client();\n if (!rmux) return false;\n const result = await rmux\n .session(target)\n .pane(0, 0)\n .sendText(text)\n .catch((error: unknown) => {\n process.emitWarning(`Could not send input to RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n return result !== undefined;\n }\n\n async capture(target: string): Promise<string | undefined> {\n const rmux = await this.client();\n if (!rmux) return undefined;\n return rmux\n .session(target)\n .pane(0, 0)\n .captureText()\n .catch((error: unknown) => {\n process.emitWarning(`Could not capture RMUX target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n }\n\n get(name: string): PtyRun | undefined {\n return this.runs.get(name);\n }\n\n readOutcome(id: string, sessionId: string): ExitResult | undefined {\n return readExitMetadata(this.auxPaths(id, sessionId).exit);\n }\n\n private trackCompletion(target: string, completion: Promise<ExitResult>): void {\n this.completions.set(target, completion);\n void completion\n .finally(() => this.completions.delete(target))\n .catch((error: unknown) => process.emitWarning(`RMUX monitor failed for ${target}: ${errorMessage(error)}`));\n }\n\n private client(): Promise<Rmux | undefined> {\n this.clientPromise ??= this.resolveClient();\n return this.clientPromise;\n }\n\n private async resolveClient(): Promise<Rmux | undefined> {\n const binaries = [bundledBinary(), 'rmux'].filter((value): value is string => value !== undefined);\n for (const binary of binaries) {\n try {\n if (binary !== 'rmux') fs.chmodSync(binary, EXECUTABLE_MODE);\n const client = new RMUX({ binary, socketName: socketName(this.paths.repositoryPath()) });\n await client.capabilities();\n return client;\n } catch (error) {\n process.emitWarning(`RMUX candidate ${binary} is unavailable: ${errorMessage(error)}`);\n // Try the next compatible candidate. Non-interactive calls can use the subprocess launcher.\n }\n }\n return undefined;\n }\n\n private prepareFiles(request: RmuxLaunchRequest, logPath: string, aux: SupervisorPaths): void {\n cleanupSupervisorFiles(aux);\n fs.writeFileSync(logPath, '', { mode: 0o600 });\n const env = environment(request.sessionId, request.interactive);\n // runnerHost supervises the command and watches the lifeline itself, so an\n // owner that dies mid-run is handled inside the pane rather than around it.\n const spec: CommandSpec = { command: request.command, cwd: request.cwd, env };\n writeCommandSpec(aux.spec, spec);\n }\n\n private auxPaths(id: string, sessionId?: string): SupervisorPaths {\n return supervisorPaths(this.paths.stateDirectory(sessionId), id);\n }\n\n /**\n * The sink the pane pipes into.\n *\n * An interactive run also asks for a raw copy: the scrubbed log it writes\n * beside it has had cursor movement taken out, which is what a terminal\n * needs most. Only interactive runs pay for it, because only they can be\n * attached to.\n */\n private logPipeCommand(logPath: string, donePath: string, request: RmuxLaunchRequest): string {\n const id = path.basename(logPath, '.log');\n return shellJoin([\n process.execPath,\n runtimeEntry('logSink'),\n logPath,\n this.paths.rotatedLogPathFor(id),\n String(getLogMaxBytes()),\n donePath,\n ...(request.interactive ? [this.paths.rawLogPathFor(id)] : []),\n ]);\n }\n\n private async monitor(rmux: Rmux, pane: Pane, target: string, aux: SupervisorPaths): Promise<ExitResult> {\n try {\n const paneCode = await waitForRunEnd(rmux, target, aux.exit);\n await waitForLogDrain(rmux, target, aux.logDone);\n return readExitMetadata(aux.exit) ?? { code: paneCode, signal: null };\n } finally {\n // The pane stays alive for log capture. Remove its session after draining,\n // allowing the last completed run to shut down the mux server too.\n let timer: ReturnType<typeof setTimeout> | undefined;\n const cleanup = async (): Promise<void> => {\n if (!(await sessionMissing(rmux, target))) await pane.server.session(target).kill();\n };\n try {\n await Promise.race([\n cleanup().catch((error: unknown) =>\n process.emitWarning(`Could not clean up completed RMUX target ${target}: ${errorMessage(error)}`),\n ),\n new Promise<void>((resolve) => {\n timer = setTimeout(() => {\n process.emitWarning(`Timed out cleaning up RMUX target ${target} after ${STOP_CLOSE_TIMEOUT_MS}ms`);\n resolve();\n }, STOP_CLOSE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n cleanupSupervisorFiles(aux);\n }\n }\n }\n\n private interactiveRun(handle: RunHandle, pane: Pane, completion: Promise<ExitResult>): PtyRun {\n let screen = '';\n let active = true;\n const handlers = new Set<(data: string) => void>();\n const refresh = async (): Promise<void> => {\n while (active) {\n const next = await pane.captureText().catch((error: unknown) => {\n process.emitWarning(`Could not capture RMUX screen for ${handle.name}: ${errorMessage(error)}`);\n return screen;\n });\n if (next !== screen) {\n screen = next;\n for (const handler of handlers) handler(next);\n }\n await delay(POLL_MS);\n }\n };\n void refresh();\n void completion\n .finally(() => {\n active = false;\n })\n .catch((error: unknown) =>\n process.emitWarning(`RMUX interactive completion failed for ${handle.name}: ${errorMessage(error)}`),\n );\n return {\n ...handle,\n write: (text) => {\n void pane.sendText(text).catch((error: unknown) => process.emitWarning(errorMessage(error)));\n },\n screen: () => screen,\n onData: (handler) => {\n handlers.add(handler);\n return () => handlers.delete(handler);\n },\n resize: (cols, rows) => {\n void pane\n .resize({ width: cols, height: rows })\n .catch((error: unknown) => process.emitWarning(errorMessage(error)));\n },\n };\n }\n}\n\nexport function rmuxPackageForTarget(platform: string, architecture: string): string | undefined {\n const target = `${platform}-${architecture}`;\n const packageName = PACKAGE_BY_PLATFORM[target];\n if (!packageName) process.emitWarning(`Bundled RMUX binary is unavailable for unsupported target ${target}`);\n return packageName;\n}\n\nexport function resolveBundledRmuxBinary(moduleUrl = import.meta.url): string | undefined {\n const configured = process.env[RMUX_BINARY_ENV];\n if (configured !== undefined && configured !== '') return configured;\n const packageName = rmuxPackageForTarget(process.platform, process.arch);\n if (!packageName) return undefined;\n try {\n const require = createRequire(moduleUrl);\n const manifest = require.resolve(`${packageName}/package.json`);\n return path.join(path.dirname(manifest), 'vendor', 'bin', 'rmux');\n } catch (error) {\n process.emitWarning(`Bundled RMUX binary is unavailable for ${packageName}: ${errorMessage(error)}`);\n return undefined;\n }\n}\n\nfunction bundledBinary(): string | undefined {\n return resolveBundledRmuxBinary();\n}\n\nfunction socketName(repositoryPath: string): string {\n const hash = createHash('sha256').update(repositoryPath).digest('hex').slice(0, SOCKET_HASH_LENGTH);\n return `${SESSION_PREFIX}${hash}`;\n}\n\nfunction environment(sessionId: string, interactive: boolean): Record<string, string> {\n const values: NodeJS.ProcessEnv = {\n ...process.env,\n ...(interactive ? {} : NON_INTERACTIVE_ENV),\n // Applied to interactive runs as well: a pane is a tty either way, and an\n // agent sending input to a prompt still has no way past a pager.\n ...NO_TERMINAL_INPUT_ENV,\n PI_SESSION_ID: sessionId,\n };\n if (!interactive) delete values['FORCE_COLOR'];\n return Object.fromEntries(\n Object.entries(values).filter((entry): entry is [string, string] => entry[1] !== undefined),\n );\n}\n\nasync function panePid(rmux: Rmux, target: string): Promise<number> {\n const response = await rmux.displayMessage(PANE_PID_FORMAT, { target });\n const message = typeof response['message'] === 'string' ? response['message'] : '';\n const pid = reportedPid(message);\n if (pid === undefined) throw new Error(`RMUX did not report a pane pid for ${target}`);\n return pid;\n}\n\n/** A pane pid as rmux prints it, or nothing when the output holds no usable pid. */\nfunction reportedPid(output: string): number | undefined {\n const pid = Number(output.trim());\n return Number.isInteger(pid) && pid > 0 ? pid : undefined;\n}\n\n/**\n * Waits for the run to end, preferring the supervisor's own record of it.\n *\n * The sidecar is written before the command's pane can end, and it outlives the\n * server that the pane's exit takes down with it. Watching for it means a run\n * that finishes inside a poll interval is observed when it finishes rather than\n * on the next tick. Asking the server stays the fallback for a supervisor that\n * died without recording anything.\n */\nasync function waitForRunEnd(rmux: Rmux, target: string, exitPath: string): Promise<number | null> {\n const landed = watchForFile(exitPath);\n let active = true;\n const readEvidence = async (): Promise<null> => {\n while (active) {\n if (await landed.appeared(POLL_MS)) return null;\n }\n return null;\n };\n const probePane = async (): Promise<number | null> => {\n while (active) {\n await delay(POLL_MS);\n if (!active) return null;\n const state = await readPaneExit(rmux, target);\n if (state.exited) return state.code;\n }\n return null;\n };\n try {\n // A stalled SDK request must not block the supervisor's durable evidence.\n // Keep only one pane request in flight, even when it never answers.\n return await Promise.race([readEvidence(), probePane()]);\n } finally {\n active = false;\n landed.close();\n }\n}\n\n/**\n * Gives the log sink a moment to mark the log complete.\n *\n * Normal completion retains the session until the sink records its final\n * writes. A lost session cannot guarantee a later marker, so it ends the wait.\n */\nasync function waitForLogDrain(rmux: Rmux, target: string, donePath: string): Promise<void> {\n const deadline = Date.now() + LOG_DRAIN_TIMEOUT_MS;\n const landed = watchForFile(donePath);\n let active = true;\n const readEvidence = async (): Promise<void> => {\n while (active && Date.now() < deadline) {\n if (await landed.appeared(LOG_DRAIN_POLL_MS)) return;\n }\n };\n const probeSession = async (): Promise<void> => {\n while (active && Date.now() < deadline) {\n await delay(LOG_DRAIN_POLL_MS);\n if (!active) return;\n if (await sessionMissing(rmux, target)) return;\n }\n };\n try {\n // The file loop enforces the deadline while a session query is outstanding.\n await Promise.race([readEvidence(), probeSession()]);\n } finally {\n active = false;\n landed.close();\n }\n}\n\nasync function closeOwnedSession(rmux: Rmux, target: string): Promise<boolean> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (result: boolean): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(result);\n };\n const timer = setTimeout(() => {\n process.emitWarning(`Timed out closing RMUX target ${target} after ${STOP_CLOSE_TIMEOUT_MS}ms`);\n finish(false);\n }, STOP_CLOSE_TIMEOUT_MS);\n void rmux\n .session(target)\n .kill()\n .then(\n () => finish(true),\n (error: unknown) => {\n process.emitWarning(`Could not close RMUX target ${target}: ${errorMessage(error)}`);\n finish(false);\n },\n );\n });\n}\n\nasync function waitForPaneStop(rmux: Rmux, target: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n try {\n if ((await readPaneExit(rmux, target)).exited) return true;\n } catch (error) {\n process.emitWarning(`Could not verify RMUX target ${target} stopped: ${errorMessage(error)}`);\n return false;\n }\n await delay(POLL_MS);\n }\n return false;\n}\n\n/**\n * One formatted read answers both questions this poll asks, so watching a run\n * costs the same single rmux call it always did.\n */\nasync function readPaneExit(rmux: Rmux, target: string): Promise<{ exited: boolean; code: number | null }> {\n let message: string;\n try {\n const response = await rmux.displayMessage(PANE_STATE_FORMAT, { target });\n message = typeof response['message'] === 'string' ? response['message'] : '';\n } catch {\n // The last session ending stops the server, so a server that cannot be\n // reached is the ordinary end of the last run rather than a fault.\n return { exited: true, code: null };\n }\n const [dead, status, ...rest] = message.trim().split(':');\n // A missing session formats as a live pane with no name rather than failing,\n // so the name is what says whether there is still anything to watch.\n if (rest.join(':') === '') return { exited: true, code: null };\n if (dead === '1' || dead === 'true') {\n const code = Number(status);\n return { exited: true, code: Number.isInteger(code) ? code : null };\n }\n return { exited: false, code: null };\n}\n\n/** A session that cannot be found, on a server that may itself be gone. */\nasync function sessionMissing(rmux: Rmux, target: string): Promise<boolean> {\n try {\n const probe = await rmux.cmd('has-session', '-t', target);\n return probe.returnCode !== 0;\n } catch {\n // No server to ask means no session to find.\n return true;\n }\n}\n\n/**\n * A pane that was already gone and a signal that could not be delivered are\n * opposite answers to \"is it stopped\", so they are reported apart.\n */\nfunction signalPane(pid: number, target: string): 'signalled' | 'gone' | 'failed' {\n try {\n process.kill(pid, 'SIGTERM');\n return 'signalled';\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ESRCH') return 'gone';\n process.emitWarning(`Could not signal RMUX target ${target}: ${errorMessage(error)}`);\n return 'failed';\n }\n}\n\nfunction readTail(target: string, maxBytes: number): string {\n let handle: number | undefined;\n try {\n const size = fs.statSync(target).size;\n const bytes = Math.min(size, maxBytes);\n const buffer = Buffer.alloc(bytes);\n handle = fs.openSync(target, 'r');\n fs.readSync(handle, buffer, 0, bytes, size - bytes);\n return buffer.toString('utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n process.emitWarning(`Could not read RMUX log ${target}: ${errorMessage(error)}`);\n }\n return '';\n } finally {\n if (handle !== undefined) fs.closeSync(handle);\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n timer.unref?.();\n });\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../constants/runnerRegistry.cjs"),n=require("../runnerRecord/index.cjs");let r=require("node:path");r=e.__toESM(r,1);let i=require("node:fs");i=e.__toESM(i,1);let a=require("@agimon-ai/doompi-core/child-process"),
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../constants/runnerRegistry.cjs"),n=require("../runnerRecord/index.cjs");let r=require("node:path");r=e.__toESM(r,1);let i=require("node:fs");i=e.__toESM(i,1);let a=require("node:child_process"),o=require("node:util"),s=require("@agimon-ai/doompi-core/child-process"),c=require("@agimon-ai/foundation-process-registry");function l(){return(0,c.createProcessRegistryService)(process.env[t.REGISTRY_PATH_ENV])}function u(e){return`session:${e}`}function d(e,t){let n=e.metadata?.[t];return typeof n==`string`?n:``}function f(e,t){let n=e.metadata?.[t];return typeof n==`number`?n:0}const p=(0,o.promisify)(a.execFile);async function m(e){if(process.platform!==`win32`)try{let{stdout:t}=await p(`ps`,[`-o`,`lstart=`,`-p`,String(e)],{encoding:`utf8`});return t.trim()||void 0}catch{return}}var h=class{paths;processControl;registry;environment;listeners=new Set;constructor(e,t,n,r){this.paths=e,this.processControl=t,this.registry=n,this.environment=r}async register(e){let n=new Date().toISOString(),r=(0,s.resolveRootSessionId)(e.sessionId,this.environment),i=await m(e.pid),a=await this.registry.registerProcess({repositoryPath:this.repositoryPath(),serviceName:`${t.SERVICE_PREFIX}${e.id}`,serviceType:t.SERVICE_TYPE,pid:e.pid,command:e.command,...i===void 0?{}:{processStartedAt:i},tags:(0,c.resolveProcessTags)([t.RUNNER_TAG,`runner:${e.id}`,u(e.sessionId)]),metadata:{id:e.id,name:e.name,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:r,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},startedAt:n,hostPid:process.pid},force:!0});if(!a.success)throw Error(a.error??`Failed to register runner ${e.name}`);let o={id:e.id,name:e.name,pid:e.pid,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:r,startedAt:n,state:`running`,promoted:!1,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},hostPid:process.pid};return this.writeRecord(o),this.notify(),o}async list(){return(await this.registry.listProcesses({repositoryPath:this.repositoryPath(),serviceType:t.SERVICE_TYPE,tags:(0,c.resolveProcessTags)([t.RUNNER_TAG])})).map(g)}async listAcrossRepositories(){return(await this.registry.listProcesses({serviceType:t.SERVICE_TYPE,tags:(0,c.resolveProcessTags)([t.RUNNER_TAG])})).map(g)}async listBySession(e){return(await this.list()).filter(t=>t.sessionId===e)}async listByRootSession(e){return(await this.list()).filter(t=>(t.rootSessionId??t.sessionId)===e)}async listAll(e){this.paths.ensureDirectories(e);let t=[],r=this.paths.stateDirectory(e);for(let e of i.default.readdirSync(r))if(v(e))try{let a=JSON.parse(i.default.readFileSync(`${r}/${e}`,`utf8`));n.isRunnerRecord(a)&&t.push(a)}catch(t){process.emitWarning(`Could not read runner metadata ${e}: ${String(t)}`)}return t.sort((e,t)=>t.startedAt.localeCompare(e.startedAt))}async get(e,t){let a=this.paths.statePathFor(e,t);try{let t=JSON.parse(i.default.readFileSync(a,`utf8`));if(!n.isRunnerRecord(t)||t.id!==e){process.emitWarning(`Could not read runner metadata ${r.default.basename(a)}: invalid runner record`);return}return t}catch(e){if(e.code===`ENOENT`)return;process.emitWarning(`Could not read runner metadata ${r.default.basename(a)}: ${String(e)}`);return}}async markPromoted(e){let t=await this.get(e);if(!t)return;let n={...t,promoted:!0};return this.writeRecord(n),this.notify(),n}async complete(e,t,n){let r=await this.get(e,n);if(r?.state===`completed`)return await this.release(e),r;let i=await this.list(),a=r??i.find(t=>t.id===e&&(n===void 0||t.sessionId===n));if(!a){n!==void 0&&i.some(t=>t.id===e&&t.sessionId!==n)||await this.release(e);return}let o={...a,state:`completed`,exit:{...t,finishedAt:new Date().toISOString()}};return this.writeRecord(o),await this.release(e),o}async release(e){let n=await this.registry.releaseProcess({repositoryPath:this.repositoryPath(),serviceName:`${t.SERVICE_PREFIX}${e}`,serviceType:t.SERVICE_TYPE,kill:!1,releasePort:!1,force:!0});if(!n.success&&!n.error?.includes(`No matching process entry`))throw Error(n.error??`Failed to release runner ${e}`);this.notify()}async pruneDead(){let e=(await this.list()).filter(e=>!this.processControl.isAlive(e.pid));for(let t of e)await this.complete(t.id,{reason:`backend_lost`,code:null,signal:null},t.sessionId);return e.map(e=>e.id)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}close(){this.listeners.clear(),this.registry.close()}notify(){for(let e of this.listeners)e()}repositoryPath(){return(0,c.normalizeRepositoryPath)(this.paths.repositoryPath())}writeRecord(e){this.paths.ensureDirectories(e.sessionId);let t=this.paths.statePathFor(e.id,e.sessionId),n=`${t}.${process.pid}.tmp`;i.default.writeFileSync(n,`${JSON.stringify(e,null,2)}\n`,`utf8`),i.default.renameSync(n,t)}};function g(e){return{id:d(e,`id`),name:d(e,`name`),pid:e.pid,command:d(e,`command`),cwd:d(e,`cwd`),logPath:d(e,`logPath`),interactive:e.metadata?.interactive===!0,sessionId:d(e,`sessionId`),...d(e,`rootSessionId`)?{rootSessionId:d(e,`rootSessionId`)}:{},startedAt:d(e,`startedAt`),state:`running`,promoted:!1,backend:_(e.metadata?.backend),...d(e,`backendTarget`)?{backendTarget:d(e,`backendTarget`)}:{},hostPid:f(e,`hostPid`)}}function _(e){return e===`rmux`||e===`tmux`?e:`native`}function v(e){return e.endsWith(`.json`)&&!e.endsWith(`.command.json`)&&!e.endsWith(`.exit.json`)}exports.RunnerRegistry=h,exports.createDefaultProcessRegistry=l;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["createProcessRegistryService","REGISTRY_PATH_ENV","resolveRootSessionId","SERVICE_PREFIX","SERVICE_TYPE","resolveProcessTags","RUNNER_TAG","fs","isRunnerRecord","path","normalizeRepositoryPath"],"sources":["../../../src/services/runnerRegistry/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { resolveRootSessionId } from '@agimon-ai/doompi-core/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\n\nimport {\n COMMAND_SIDECAR_SUFFIX,\n EXIT_SIDECAR_SUFFIX,\n JSON_EXTENSION,\n NOT_FOUND_ERROR_CODE,\n REGISTRY_PATH_ENV,\n RUNNER_TAG,\n SERVICE_PREFIX,\n SERVICE_TYPE,\n} from '../../constants/runnerRegistry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** The slice of the shared registry service this wrapper depends on. */\nexport type ProcessRegistryPort = Pick<\n ProcessRegistryService,\n 'registerProcess' | 'releaseProcess' | 'listProcesses' | 'close'\n>;\n\n/** Opens the SQLite-backed registry the rest of the monorepo already shares. */\nexport function createDefaultProcessRegistry(): ProcessRegistryPort {\n return createProcessRegistryService(process.env[REGISTRY_PATH_ENV]);\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction readString(record: ProcessRegistryRecord, key: string): string {\n const value = record.metadata?.[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction readNumber(record: ProcessRegistryRecord, key: string): number {\n const value = record.metadata?.[key];\n return typeof value === 'number' ? value : 0;\n}\n\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly listeners = new Set<() => void>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n private readonly environment: Readonly<Record<string, string | undefined>>,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId, this.environment);\n const response = await this.registry.registerProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${input.id}`,\n serviceType: SERVICE_TYPE,\n pid: input.pid,\n command: input.command,\n tags: resolveProcessTags([RUNNER_TAG, `runner:${input.id}`, sessionTag(input.sessionId)]),\n metadata: {\n id: input.id,\n name: input.name,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n startedAt,\n hostPid: process.pid,\n },\n // The caller has already resolved name collisions against live records,\n // so anything still holding this name is stale.\n force: true,\n });\n if (!response.success) throw new Error(response.error ?? `Failed to register runner ${input.name}`);\n const record: RunnerRecord = {\n id: input.id,\n name: input.name,\n pid: input.pid,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n startedAt,\n state: 'running',\n promoted: false,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n hostPid: process.pid,\n };\n this.writeRecord(record);\n this.notify();\n return record;\n }\n\n async list(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n repositoryPath: this.repositoryPath(),\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listAcrossRepositories(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listBySession(sessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => record.sessionId === sessionId);\n }\n\n async listByRootSession(rootSessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId);\n }\n\n async listAll(sessionId?: string): Promise<RunnerRecord[]> {\n this.paths.ensureDirectories(sessionId);\n const records: RunnerRecord[] = [];\n const directory = this.paths.stateDirectory(sessionId);\n for (const entry of fs.readdirSync(directory)) {\n if (!isPrimaryMetadataEntry(entry)) continue;\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(`${directory}/${entry}`, 'utf8'));\n if (isRunnerRecord(parsed)) records.push(parsed);\n } catch (error) {\n process.emitWarning(`Could not read runner metadata ${entry}: ${String(error)}`);\n }\n }\n return records.sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n }\n\n async get(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const target = this.paths.statePathFor(id, sessionId);\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (!isRunnerRecord(parsed) || parsed.id !== id) {\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: invalid runner record`);\n return undefined;\n }\n return parsed;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return undefined;\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: ${String(error)}`);\n return undefined;\n }\n }\n\n async markPromoted(id: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record) return undefined;\n const promoted = { ...record, promoted: true };\n this.writeRecord(promoted);\n this.notify();\n return promoted;\n }\n\n async complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined> {\n const persisted = await this.get(id, sessionId);\n if (persisted?.state === 'completed') {\n await this.release(id);\n return persisted;\n }\n const active = await this.list();\n const record =\n persisted ??\n active.find((candidate) => candidate.id === id && (sessionId === undefined || candidate.sessionId === sessionId));\n if (!record) {\n const belongsToAnotherSession =\n sessionId !== undefined && active.some((candidate) => candidate.id === id && candidate.sessionId !== sessionId);\n if (!belongsToAnotherSession) await this.release(id);\n return undefined;\n }\n const completed: RunnerRecord = {\n ...record,\n state: 'completed',\n exit: { ...outcome, finishedAt: new Date().toISOString() },\n };\n this.writeRecord(completed);\n await this.release(id);\n return completed;\n }\n\n async release(id: string): Promise<void> {\n const response = await this.registry.releaseProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${id}`,\n serviceType: SERVICE_TYPE,\n kill: false,\n releasePort: false,\n force: true,\n });\n if (!response.success && !response.error?.includes('No matching process entry')) {\n throw new Error(response.error ?? `Failed to release runner ${id}`);\n }\n this.notify();\n }\n\n async pruneDead(): Promise<string[]> {\n const dead = (await this.list()).filter((record) => !this.processControl.isAlive(record.pid));\n for (const record of dead) {\n await this.complete(record.id, { reason: 'backend_lost', code: null, signal: null }, record.sessionId);\n }\n return dead.map((record) => record.id);\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n close(): void {\n this.listeners.clear();\n this.registry.close();\n }\n\n private notify(): void {\n for (const listener of this.listeners) listener();\n }\n\n private repositoryPath(): string {\n return normalizeRepositoryPath(this.paths.repositoryPath());\n }\n\n private writeRecord(record: RunnerRecord): void {\n this.paths.ensureDirectories(record.sessionId);\n const target = this.paths.statePathFor(record.id, record.sessionId);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\\n`, 'utf8');\n fs.renameSync(temporary, target);\n }\n}\n\n/** Every field is read back from metadata this class wrote on registration. */\nfunction toRunnerRecord(record: ProcessRegistryRecord): RunnerRecord {\n return {\n id: readString(record, 'id'),\n name: readString(record, 'name'),\n pid: record.pid,\n command: readString(record, 'command'),\n cwd: readString(record, 'cwd'),\n logPath: readString(record, 'logPath'),\n interactive: record.metadata?.interactive === true,\n sessionId: readString(record, 'sessionId'),\n ...(readString(record, 'rootSessionId') ? { rootSessionId: readString(record, 'rootSessionId') } : {}),\n startedAt: readString(record, 'startedAt'),\n state: 'running',\n promoted: false,\n backend: readBackend(record.metadata?.backend),\n ...(readString(record, 'backendTarget') ? { backendTarget: readString(record, 'backendTarget') } : {}),\n hostPid: readNumber(record, 'hostPid'),\n };\n}\n\nfunction readBackend(value: unknown): RunnerRecord['backend'] {\n return value === 'rmux' || value === 'tmux' ? value : 'native';\n}\n\nfunction isPrimaryMetadataEntry(entry: string): boolean {\n return (\n entry.endsWith(JSON_EXTENSION) && !entry.endsWith(COMMAND_SIDECAR_SUFFIX) && !entry.endsWith(EXIT_SIDECAR_SUFFIX)\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["createProcessRegistryService","REGISTRY_PATH_ENV","promisify","execFile","resolveRootSessionId","SERVICE_PREFIX","SERVICE_TYPE","resolveProcessTags","RUNNER_TAG","fs","isRunnerRecord","path","normalizeRepositoryPath"],"sources":["../../../src/services/runnerRegistry/index.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport { resolveRootSessionId } from '@agimon-ai/doompi-core/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\n\nimport {\n COMMAND_SIDECAR_SUFFIX,\n EXIT_SIDECAR_SUFFIX,\n JSON_EXTENSION,\n NOT_FOUND_ERROR_CODE,\n REGISTRY_PATH_ENV,\n RUNNER_TAG,\n SERVICE_PREFIX,\n SERVICE_TYPE,\n} from '../../constants/runnerRegistry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** The slice of the shared registry service this wrapper depends on. */\nexport type ProcessRegistryPort = Pick<\n ProcessRegistryService,\n 'registerProcess' | 'releaseProcess' | 'listProcesses' | 'close'\n>;\n\n/** Opens the SQLite-backed registry the rest of the monorepo already shares. */\nexport function createDefaultProcessRegistry(): ProcessRegistryPort {\n return createProcessRegistryService(process.env[REGISTRY_PATH_ENV]);\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction readString(record: ProcessRegistryRecord, key: string): string {\n const value = record.metadata?.[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction readNumber(record: ProcessRegistryRecord, key: string): number {\n const value = record.metadata?.[key];\n return typeof value === 'number' ? value : 0;\n}\n\nconst runCommand = promisify(execFile);\n\n/**\n * The process start time the registry stores to prove a pid is still the\n * process it registered.\n *\n * The registry resolves it itself when the caller omits it, and it does so with\n * `execFileSync`, which stops this process for the length of a `ps` on every\n * runner that starts. Reading it here costs the same subprocess and blocks\n * nothing: the command is already running by the time a runner is registered.\n *\n * The value has to match what the registry would have read, so the command and\n * the trimming are deliberately identical to its own.\n */\nasync function processStartedAt(pid: number): Promise<string | undefined> {\n if (process.platform === 'win32') return undefined;\n try {\n const { stdout } = await runCommand('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8' });\n return stdout.trim() || undefined;\n } catch {\n return undefined;\n }\n}\n\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly listeners = new Set<() => void>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n private readonly environment: Readonly<Record<string, string | undefined>>,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId, this.environment);\n const identity = await processStartedAt(input.pid);\n const response = await this.registry.registerProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${input.id}`,\n serviceType: SERVICE_TYPE,\n pid: input.pid,\n command: input.command,\n ...(identity === undefined ? {} : { processStartedAt: identity }),\n tags: resolveProcessTags([RUNNER_TAG, `runner:${input.id}`, sessionTag(input.sessionId)]),\n metadata: {\n id: input.id,\n name: input.name,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n startedAt,\n hostPid: process.pid,\n },\n // The caller has already resolved name collisions against live records,\n // so anything still holding this name is stale.\n force: true,\n });\n if (!response.success) throw new Error(response.error ?? `Failed to register runner ${input.name}`);\n const record: RunnerRecord = {\n id: input.id,\n name: input.name,\n pid: input.pid,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n startedAt,\n state: 'running',\n promoted: false,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n hostPid: process.pid,\n };\n this.writeRecord(record);\n this.notify();\n return record;\n }\n\n async list(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n repositoryPath: this.repositoryPath(),\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listAcrossRepositories(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listBySession(sessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => record.sessionId === sessionId);\n }\n\n async listByRootSession(rootSessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId);\n }\n\n async listAll(sessionId?: string): Promise<RunnerRecord[]> {\n this.paths.ensureDirectories(sessionId);\n const records: RunnerRecord[] = [];\n const directory = this.paths.stateDirectory(sessionId);\n for (const entry of fs.readdirSync(directory)) {\n if (!isPrimaryMetadataEntry(entry)) continue;\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(`${directory}/${entry}`, 'utf8'));\n if (isRunnerRecord(parsed)) records.push(parsed);\n } catch (error) {\n process.emitWarning(`Could not read runner metadata ${entry}: ${String(error)}`);\n }\n }\n return records.sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n }\n\n async get(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const target = this.paths.statePathFor(id, sessionId);\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (!isRunnerRecord(parsed) || parsed.id !== id) {\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: invalid runner record`);\n return undefined;\n }\n return parsed;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return undefined;\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: ${String(error)}`);\n return undefined;\n }\n }\n\n async markPromoted(id: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record) return undefined;\n const promoted = { ...record, promoted: true };\n this.writeRecord(promoted);\n this.notify();\n return promoted;\n }\n\n async complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined> {\n const persisted = await this.get(id, sessionId);\n if (persisted?.state === 'completed') {\n await this.release(id);\n return persisted;\n }\n const active = await this.list();\n const record =\n persisted ??\n active.find((candidate) => candidate.id === id && (sessionId === undefined || candidate.sessionId === sessionId));\n if (!record) {\n const belongsToAnotherSession =\n sessionId !== undefined && active.some((candidate) => candidate.id === id && candidate.sessionId !== sessionId);\n if (!belongsToAnotherSession) await this.release(id);\n return undefined;\n }\n const completed: RunnerRecord = {\n ...record,\n state: 'completed',\n exit: { ...outcome, finishedAt: new Date().toISOString() },\n };\n this.writeRecord(completed);\n await this.release(id);\n return completed;\n }\n\n async release(id: string): Promise<void> {\n const response = await this.registry.releaseProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${id}`,\n serviceType: SERVICE_TYPE,\n kill: false,\n releasePort: false,\n force: true,\n });\n if (!response.success && !response.error?.includes('No matching process entry')) {\n throw new Error(response.error ?? `Failed to release runner ${id}`);\n }\n this.notify();\n }\n\n async pruneDead(): Promise<string[]> {\n const dead = (await this.list()).filter((record) => !this.processControl.isAlive(record.pid));\n for (const record of dead) {\n await this.complete(record.id, { reason: 'backend_lost', code: null, signal: null }, record.sessionId);\n }\n return dead.map((record) => record.id);\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n close(): void {\n this.listeners.clear();\n this.registry.close();\n }\n\n private notify(): void {\n for (const listener of this.listeners) listener();\n }\n\n private repositoryPath(): string {\n return normalizeRepositoryPath(this.paths.repositoryPath());\n }\n\n private writeRecord(record: RunnerRecord): void {\n this.paths.ensureDirectories(record.sessionId);\n const target = this.paths.statePathFor(record.id, record.sessionId);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\\n`, 'utf8');\n fs.renameSync(temporary, target);\n }\n}\n\n/** Every field is read back from metadata this class wrote on registration. */\nfunction toRunnerRecord(record: ProcessRegistryRecord): RunnerRecord {\n return {\n id: readString(record, 'id'),\n name: readString(record, 'name'),\n pid: record.pid,\n command: readString(record, 'command'),\n cwd: readString(record, 'cwd'),\n logPath: readString(record, 'logPath'),\n interactive: record.metadata?.interactive === true,\n sessionId: readString(record, 'sessionId'),\n ...(readString(record, 'rootSessionId') ? { rootSessionId: readString(record, 'rootSessionId') } : {}),\n startedAt: readString(record, 'startedAt'),\n state: 'running',\n promoted: false,\n backend: readBackend(record.metadata?.backend),\n ...(readString(record, 'backendTarget') ? { backendTarget: readString(record, 'backendTarget') } : {}),\n hostPid: readNumber(record, 'hostPid'),\n };\n}\n\nfunction readBackend(value: unknown): RunnerRecord['backend'] {\n return value === 'rmux' || value === 'tmux' ? value : 'native';\n}\n\nfunction isPrimaryMetadataEntry(entry: string): boolean {\n return (\n entry.endsWith(JSON_EXTENSION) && !entry.endsWith(COMMAND_SIDECAR_SUFFIX) && !entry.endsWith(EXIT_SIDECAR_SUFFIX)\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/services/runnerRegistry/index.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/services/runnerRegistry/index.ts"],"mappings":";;;;;;YAmCY,sBAAsB,KAChC;;wBAKc,gCAAgC;qBA0CnC,0BAA0B;mBAIlB;mBACA;mBACA;mBACA;mBANF;EAEjB,YACmB,OAAO,cACP,gBAAgB,iBAChB,UAAU,qBACV,aAAa,SAAS;EAGnC,SAAS,OAAO,sBAAsB,QAAQ;EAqD9C,QAAQ,QAAQ;EAShB,0BAA0B,QAAQ;EAQlC,cAAc,oBAAoB,QAAQ;EAK1C,kBAAkB,wBAAwB,QAAQ;EAKlD,QAAQ,qBAAqB,QAAQ;EAgBrC,IAAI,YAAY,qBAAqB,QAAQ;EAgB7C,aAAa,aAAa,QAAQ;EASlC,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;EA0BhF,QAAQ,aAAa;EAerB,aAAa;EAQnB,UAAU;EAKV;UAKQ;UAIA;UAIA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/runnerRegistry/index.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/runnerRegistry/index.ts"],"mappings":";;;;;;YAmCY,sBAAsB,KAChC;;wBAKc,gCAAgC;qBA0CnC,0BAA0B;mBAIlB;mBACA;mBACA;mBACA;mBANF;EAEjB,YACmB,OAAO,cACP,gBAAgB,iBAChB,UAAU,qBACV,aAAa,SAAS;EAGnC,SAAS,OAAO,sBAAsB,QAAQ;EAqD9C,QAAQ,QAAQ;EAShB,0BAA0B,QAAQ;EAQlC,cAAc,oBAAoB,QAAQ;EAK1C,kBAAkB,wBAAwB,QAAQ;EAKlD,QAAQ,qBAAqB,QAAQ;EAgBrC,IAAI,YAAY,qBAAqB,QAAQ;EAgB7C,aAAa,aAAa,QAAQ;EASlC,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;EA0BhF,QAAQ,aAAa;EAerB,aAAa;EAQnB,UAAU;EAKV;UAKQ;UAIA;UAIA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{REGISTRY_PATH_ENV as e,RUNNER_TAG as t,SERVICE_PREFIX as n,SERVICE_TYPE as r}from"../../constants/runnerRegistry.mjs";import{isRunnerRecord as i}from"../runnerRecord/index.mjs";import a from"node:path";import o from"node:fs";import{
|
|
1
|
+
import{REGISTRY_PATH_ENV as e,RUNNER_TAG as t,SERVICE_PREFIX as n,SERVICE_TYPE as r}from"../../constants/runnerRegistry.mjs";import{isRunnerRecord as i}from"../runnerRecord/index.mjs";import a from"node:path";import o from"node:fs";import{execFile as s}from"node:child_process";import{promisify as c}from"node:util";import{resolveRootSessionId as l}from"@agimon-ai/doompi-core/child-process";import{createProcessRegistryService as u,normalizeRepositoryPath as d,resolveProcessTags as f}from"@agimon-ai/foundation-process-registry";function p(){return u(process.env[e])}function m(e){return`session:${e}`}function h(e,t){let n=e.metadata?.[t];return typeof n==`string`?n:``}function g(e,t){let n=e.metadata?.[t];return typeof n==`number`?n:0}const _=c(s);async function v(e){if(process.platform!==`win32`)try{let{stdout:t}=await _(`ps`,[`-o`,`lstart=`,`-p`,String(e)],{encoding:`utf8`});return t.trim()||void 0}catch{return}}var y=class{paths;processControl;registry;environment;listeners=new Set;constructor(e,t,n,r){this.paths=e,this.processControl=t,this.registry=n,this.environment=r}async register(e){let i=new Date().toISOString(),a=l(e.sessionId,this.environment),o=await v(e.pid),s=await this.registry.registerProcess({repositoryPath:this.repositoryPath(),serviceName:`${n}${e.id}`,serviceType:r,pid:e.pid,command:e.command,...o===void 0?{}:{processStartedAt:o},tags:f([t,`runner:${e.id}`,m(e.sessionId)]),metadata:{id:e.id,name:e.name,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:a,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},startedAt:i,hostPid:process.pid},force:!0});if(!s.success)throw Error(s.error??`Failed to register runner ${e.name}`);let c={id:e.id,name:e.name,pid:e.pid,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:a,startedAt:i,state:`running`,promoted:!1,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},hostPid:process.pid};return this.writeRecord(c),this.notify(),c}async list(){return(await this.registry.listProcesses({repositoryPath:this.repositoryPath(),serviceType:r,tags:f([t])})).map(b)}async listAcrossRepositories(){return(await this.registry.listProcesses({serviceType:r,tags:f([t])})).map(b)}async listBySession(e){return(await this.list()).filter(t=>t.sessionId===e)}async listByRootSession(e){return(await this.list()).filter(t=>(t.rootSessionId??t.sessionId)===e)}async listAll(e){this.paths.ensureDirectories(e);let t=[],n=this.paths.stateDirectory(e);for(let e of o.readdirSync(n))if(S(e))try{let r=JSON.parse(o.readFileSync(`${n}/${e}`,`utf8`));i(r)&&t.push(r)}catch(t){process.emitWarning(`Could not read runner metadata ${e}: ${String(t)}`)}return t.sort((e,t)=>t.startedAt.localeCompare(e.startedAt))}async get(e,t){let n=this.paths.statePathFor(e,t);try{let t=JSON.parse(o.readFileSync(n,`utf8`));if(!i(t)||t.id!==e){process.emitWarning(`Could not read runner metadata ${a.basename(n)}: invalid runner record`);return}return t}catch(e){if(e.code===`ENOENT`)return;process.emitWarning(`Could not read runner metadata ${a.basename(n)}: ${String(e)}`);return}}async markPromoted(e){let t=await this.get(e);if(!t)return;let n={...t,promoted:!0};return this.writeRecord(n),this.notify(),n}async complete(e,t,n){let r=await this.get(e,n);if(r?.state===`completed`)return await this.release(e),r;let i=await this.list(),a=r??i.find(t=>t.id===e&&(n===void 0||t.sessionId===n));if(!a){n!==void 0&&i.some(t=>t.id===e&&t.sessionId!==n)||await this.release(e);return}let o={...a,state:`completed`,exit:{...t,finishedAt:new Date().toISOString()}};return this.writeRecord(o),await this.release(e),o}async release(e){let t=await this.registry.releaseProcess({repositoryPath:this.repositoryPath(),serviceName:`${n}${e}`,serviceType:r,kill:!1,releasePort:!1,force:!0});if(!t.success&&!t.error?.includes(`No matching process entry`))throw Error(t.error??`Failed to release runner ${e}`);this.notify()}async pruneDead(){let e=(await this.list()).filter(e=>!this.processControl.isAlive(e.pid));for(let t of e)await this.complete(t.id,{reason:`backend_lost`,code:null,signal:null},t.sessionId);return e.map(e=>e.id)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}close(){this.listeners.clear(),this.registry.close()}notify(){for(let e of this.listeners)e()}repositoryPath(){return d(this.paths.repositoryPath())}writeRecord(e){this.paths.ensureDirectories(e.sessionId);let t=this.paths.statePathFor(e.id,e.sessionId),n=`${t}.${process.pid}.tmp`;o.writeFileSync(n,`${JSON.stringify(e,null,2)}\n`,`utf8`),o.renameSync(n,t)}};function b(e){return{id:h(e,`id`),name:h(e,`name`),pid:e.pid,command:h(e,`command`),cwd:h(e,`cwd`),logPath:h(e,`logPath`),interactive:e.metadata?.interactive===!0,sessionId:h(e,`sessionId`),...h(e,`rootSessionId`)?{rootSessionId:h(e,`rootSessionId`)}:{},startedAt:h(e,`startedAt`),state:`running`,promoted:!1,backend:x(e.metadata?.backend),...h(e,`backendTarget`)?{backendTarget:h(e,`backendTarget`)}:{},hostPid:g(e,`hostPid`)}}function x(e){return e===`rmux`||e===`tmux`?e:`native`}function S(e){return e.endsWith(`.json`)&&!e.endsWith(`.command.json`)&&!e.endsWith(`.exit.json`)}export{y as RunnerRegistry,p as createDefaultProcessRegistry};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/runnerRegistry/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { resolveRootSessionId } from '@agimon-ai/doompi-core/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\n\nimport {\n COMMAND_SIDECAR_SUFFIX,\n EXIT_SIDECAR_SUFFIX,\n JSON_EXTENSION,\n NOT_FOUND_ERROR_CODE,\n REGISTRY_PATH_ENV,\n RUNNER_TAG,\n SERVICE_PREFIX,\n SERVICE_TYPE,\n} from '../../constants/runnerRegistry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** The slice of the shared registry service this wrapper depends on. */\nexport type ProcessRegistryPort = Pick<\n ProcessRegistryService,\n 'registerProcess' | 'releaseProcess' | 'listProcesses' | 'close'\n>;\n\n/** Opens the SQLite-backed registry the rest of the monorepo already shares. */\nexport function createDefaultProcessRegistry(): ProcessRegistryPort {\n return createProcessRegistryService(process.env[REGISTRY_PATH_ENV]);\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction readString(record: ProcessRegistryRecord, key: string): string {\n const value = record.metadata?.[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction readNumber(record: ProcessRegistryRecord, key: string): number {\n const value = record.metadata?.[key];\n return typeof value === 'number' ? value : 0;\n}\n\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly listeners = new Set<() => void>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n private readonly environment: Readonly<Record<string, string | undefined>>,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId, this.environment);\n const response = await this.registry.registerProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${input.id}`,\n serviceType: SERVICE_TYPE,\n pid: input.pid,\n command: input.command,\n tags: resolveProcessTags([RUNNER_TAG, `runner:${input.id}`, sessionTag(input.sessionId)]),\n metadata: {\n id: input.id,\n name: input.name,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n startedAt,\n hostPid: process.pid,\n },\n // The caller has already resolved name collisions against live records,\n // so anything still holding this name is stale.\n force: true,\n });\n if (!response.success) throw new Error(response.error ?? `Failed to register runner ${input.name}`);\n const record: RunnerRecord = {\n id: input.id,\n name: input.name,\n pid: input.pid,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n startedAt,\n state: 'running',\n promoted: false,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n hostPid: process.pid,\n };\n this.writeRecord(record);\n this.notify();\n return record;\n }\n\n async list(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n repositoryPath: this.repositoryPath(),\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listAcrossRepositories(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listBySession(sessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => record.sessionId === sessionId);\n }\n\n async listByRootSession(rootSessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId);\n }\n\n async listAll(sessionId?: string): Promise<RunnerRecord[]> {\n this.paths.ensureDirectories(sessionId);\n const records: RunnerRecord[] = [];\n const directory = this.paths.stateDirectory(sessionId);\n for (const entry of fs.readdirSync(directory)) {\n if (!isPrimaryMetadataEntry(entry)) continue;\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(`${directory}/${entry}`, 'utf8'));\n if (isRunnerRecord(parsed)) records.push(parsed);\n } catch (error) {\n process.emitWarning(`Could not read runner metadata ${entry}: ${String(error)}`);\n }\n }\n return records.sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n }\n\n async get(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const target = this.paths.statePathFor(id, sessionId);\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (!isRunnerRecord(parsed) || parsed.id !== id) {\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: invalid runner record`);\n return undefined;\n }\n return parsed;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return undefined;\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: ${String(error)}`);\n return undefined;\n }\n }\n\n async markPromoted(id: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record) return undefined;\n const promoted = { ...record, promoted: true };\n this.writeRecord(promoted);\n this.notify();\n return promoted;\n }\n\n async complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined> {\n const persisted = await this.get(id, sessionId);\n if (persisted?.state === 'completed') {\n await this.release(id);\n return persisted;\n }\n const active = await this.list();\n const record =\n persisted ??\n active.find((candidate) => candidate.id === id && (sessionId === undefined || candidate.sessionId === sessionId));\n if (!record) {\n const belongsToAnotherSession =\n sessionId !== undefined && active.some((candidate) => candidate.id === id && candidate.sessionId !== sessionId);\n if (!belongsToAnotherSession) await this.release(id);\n return undefined;\n }\n const completed: RunnerRecord = {\n ...record,\n state: 'completed',\n exit: { ...outcome, finishedAt: new Date().toISOString() },\n };\n this.writeRecord(completed);\n await this.release(id);\n return completed;\n }\n\n async release(id: string): Promise<void> {\n const response = await this.registry.releaseProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${id}`,\n serviceType: SERVICE_TYPE,\n kill: false,\n releasePort: false,\n force: true,\n });\n if (!response.success && !response.error?.includes('No matching process entry')) {\n throw new Error(response.error ?? `Failed to release runner ${id}`);\n }\n this.notify();\n }\n\n async pruneDead(): Promise<string[]> {\n const dead = (await this.list()).filter((record) => !this.processControl.isAlive(record.pid));\n for (const record of dead) {\n await this.complete(record.id, { reason: 'backend_lost', code: null, signal: null }, record.sessionId);\n }\n return dead.map((record) => record.id);\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n close(): void {\n this.listeners.clear();\n this.registry.close();\n }\n\n private notify(): void {\n for (const listener of this.listeners) listener();\n }\n\n private repositoryPath(): string {\n return normalizeRepositoryPath(this.paths.repositoryPath());\n }\n\n private writeRecord(record: RunnerRecord): void {\n this.paths.ensureDirectories(record.sessionId);\n const target = this.paths.statePathFor(record.id, record.sessionId);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\\n`, 'utf8');\n fs.renameSync(temporary, target);\n }\n}\n\n/** Every field is read back from metadata this class wrote on registration. */\nfunction toRunnerRecord(record: ProcessRegistryRecord): RunnerRecord {\n return {\n id: readString(record, 'id'),\n name: readString(record, 'name'),\n pid: record.pid,\n command: readString(record, 'command'),\n cwd: readString(record, 'cwd'),\n logPath: readString(record, 'logPath'),\n interactive: record.metadata?.interactive === true,\n sessionId: readString(record, 'sessionId'),\n ...(readString(record, 'rootSessionId') ? { rootSessionId: readString(record, 'rootSessionId') } : {}),\n startedAt: readString(record, 'startedAt'),\n state: 'running',\n promoted: false,\n backend: readBackend(record.metadata?.backend),\n ...(readString(record, 'backendTarget') ? { backendTarget: readString(record, 'backendTarget') } : {}),\n hostPid: readNumber(record, 'hostPid'),\n };\n}\n\nfunction readBackend(value: unknown): RunnerRecord['backend'] {\n return value === 'rmux' || value === 'tmux' ? value : 'native';\n}\n\nfunction isPrimaryMetadataEntry(entry: string): boolean {\n return (\n entry.endsWith(JSON_EXTENSION) && !entry.endsWith(COMMAND_SIDECAR_SUFFIX) && !entry.endsWith(EXIT_SIDECAR_SUFFIX)\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/runnerRegistry/index.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport { resolveRootSessionId } from '@agimon-ai/doompi-core/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\n\nimport {\n COMMAND_SIDECAR_SUFFIX,\n EXIT_SIDECAR_SUFFIX,\n JSON_EXTENSION,\n NOT_FOUND_ERROR_CODE,\n REGISTRY_PATH_ENV,\n RUNNER_TAG,\n SERVICE_PREFIX,\n SERVICE_TYPE,\n} from '../../constants/runnerRegistry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** The slice of the shared registry service this wrapper depends on. */\nexport type ProcessRegistryPort = Pick<\n ProcessRegistryService,\n 'registerProcess' | 'releaseProcess' | 'listProcesses' | 'close'\n>;\n\n/** Opens the SQLite-backed registry the rest of the monorepo already shares. */\nexport function createDefaultProcessRegistry(): ProcessRegistryPort {\n return createProcessRegistryService(process.env[REGISTRY_PATH_ENV]);\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction readString(record: ProcessRegistryRecord, key: string): string {\n const value = record.metadata?.[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction readNumber(record: ProcessRegistryRecord, key: string): number {\n const value = record.metadata?.[key];\n return typeof value === 'number' ? value : 0;\n}\n\nconst runCommand = promisify(execFile);\n\n/**\n * The process start time the registry stores to prove a pid is still the\n * process it registered.\n *\n * The registry resolves it itself when the caller omits it, and it does so with\n * `execFileSync`, which stops this process for the length of a `ps` on every\n * runner that starts. Reading it here costs the same subprocess and blocks\n * nothing: the command is already running by the time a runner is registered.\n *\n * The value has to match what the registry would have read, so the command and\n * the trimming are deliberately identical to its own.\n */\nasync function processStartedAt(pid: number): Promise<string | undefined> {\n if (process.platform === 'win32') return undefined;\n try {\n const { stdout } = await runCommand('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8' });\n return stdout.trim() || undefined;\n } catch {\n return undefined;\n }\n}\n\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly listeners = new Set<() => void>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n private readonly environment: Readonly<Record<string, string | undefined>>,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId, this.environment);\n const identity = await processStartedAt(input.pid);\n const response = await this.registry.registerProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${input.id}`,\n serviceType: SERVICE_TYPE,\n pid: input.pid,\n command: input.command,\n ...(identity === undefined ? {} : { processStartedAt: identity }),\n tags: resolveProcessTags([RUNNER_TAG, `runner:${input.id}`, sessionTag(input.sessionId)]),\n metadata: {\n id: input.id,\n name: input.name,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n startedAt,\n hostPid: process.pid,\n },\n // The caller has already resolved name collisions against live records,\n // so anything still holding this name is stale.\n force: true,\n });\n if (!response.success) throw new Error(response.error ?? `Failed to register runner ${input.name}`);\n const record: RunnerRecord = {\n id: input.id,\n name: input.name,\n pid: input.pid,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n startedAt,\n state: 'running',\n promoted: false,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n hostPid: process.pid,\n };\n this.writeRecord(record);\n this.notify();\n return record;\n }\n\n async list(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n repositoryPath: this.repositoryPath(),\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listAcrossRepositories(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listBySession(sessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => record.sessionId === sessionId);\n }\n\n async listByRootSession(rootSessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId);\n }\n\n async listAll(sessionId?: string): Promise<RunnerRecord[]> {\n this.paths.ensureDirectories(sessionId);\n const records: RunnerRecord[] = [];\n const directory = this.paths.stateDirectory(sessionId);\n for (const entry of fs.readdirSync(directory)) {\n if (!isPrimaryMetadataEntry(entry)) continue;\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(`${directory}/${entry}`, 'utf8'));\n if (isRunnerRecord(parsed)) records.push(parsed);\n } catch (error) {\n process.emitWarning(`Could not read runner metadata ${entry}: ${String(error)}`);\n }\n }\n return records.sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n }\n\n async get(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const target = this.paths.statePathFor(id, sessionId);\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (!isRunnerRecord(parsed) || parsed.id !== id) {\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: invalid runner record`);\n return undefined;\n }\n return parsed;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return undefined;\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: ${String(error)}`);\n return undefined;\n }\n }\n\n async markPromoted(id: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record) return undefined;\n const promoted = { ...record, promoted: true };\n this.writeRecord(promoted);\n this.notify();\n return promoted;\n }\n\n async complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined> {\n const persisted = await this.get(id, sessionId);\n if (persisted?.state === 'completed') {\n await this.release(id);\n return persisted;\n }\n const active = await this.list();\n const record =\n persisted ??\n active.find((candidate) => candidate.id === id && (sessionId === undefined || candidate.sessionId === sessionId));\n if (!record) {\n const belongsToAnotherSession =\n sessionId !== undefined && active.some((candidate) => candidate.id === id && candidate.sessionId !== sessionId);\n if (!belongsToAnotherSession) await this.release(id);\n return undefined;\n }\n const completed: RunnerRecord = {\n ...record,\n state: 'completed',\n exit: { ...outcome, finishedAt: new Date().toISOString() },\n };\n this.writeRecord(completed);\n await this.release(id);\n return completed;\n }\n\n async release(id: string): Promise<void> {\n const response = await this.registry.releaseProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${id}`,\n serviceType: SERVICE_TYPE,\n kill: false,\n releasePort: false,\n force: true,\n });\n if (!response.success && !response.error?.includes('No matching process entry')) {\n throw new Error(response.error ?? `Failed to release runner ${id}`);\n }\n this.notify();\n }\n\n async pruneDead(): Promise<string[]> {\n const dead = (await this.list()).filter((record) => !this.processControl.isAlive(record.pid));\n for (const record of dead) {\n await this.complete(record.id, { reason: 'backend_lost', code: null, signal: null }, record.sessionId);\n }\n return dead.map((record) => record.id);\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n close(): void {\n this.listeners.clear();\n this.registry.close();\n }\n\n private notify(): void {\n for (const listener of this.listeners) listener();\n }\n\n private repositoryPath(): string {\n return normalizeRepositoryPath(this.paths.repositoryPath());\n }\n\n private writeRecord(record: RunnerRecord): void {\n this.paths.ensureDirectories(record.sessionId);\n const target = this.paths.statePathFor(record.id, record.sessionId);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\\n`, 'utf8');\n fs.renameSync(temporary, target);\n }\n}\n\n/** Every field is read back from metadata this class wrote on registration. */\nfunction toRunnerRecord(record: ProcessRegistryRecord): RunnerRecord {\n return {\n id: readString(record, 'id'),\n name: readString(record, 'name'),\n pid: record.pid,\n command: readString(record, 'command'),\n cwd: readString(record, 'cwd'),\n logPath: readString(record, 'logPath'),\n interactive: record.metadata?.interactive === true,\n sessionId: readString(record, 'sessionId'),\n ...(readString(record, 'rootSessionId') ? { rootSessionId: readString(record, 'rootSessionId') } : {}),\n startedAt: readString(record, 'startedAt'),\n state: 'running',\n promoted: false,\n backend: readBackend(record.metadata?.backend),\n ...(readString(record, 'backendTarget') ? { backendTarget: readString(record, 'backendTarget') } : {}),\n hostPid: readNumber(record, 'hostPid'),\n };\n}\n\nfunction readBackend(value: unknown): RunnerRecord['backend'] {\n return value === 'rmux' || value === 'tmux' ? value : 'native';\n}\n\nfunction isPrimaryMetadataEntry(entry: string): boolean {\n return (\n entry.endsWith(JSON_EXTENSION) && !entry.endsWith(COMMAND_SIDECAR_SUFFIX) && !entry.endsWith(EXIT_SIDECAR_SUFFIX)\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:url");const i={GIT_PAGER:`cat`,PAGER:`cat`,GIT_TERMINAL_PROMPT:`0`},a=`ENOENT`;function o(e,n){return{spec:t.default.join(e,`${n}.command.json`),gate:t.default.join(e,`${n}.gate`),exit:t.default.join(e,`${n}.exit.json`),logDone:t.default.join(e,`${n}.log.done`)}}function s(e,t){n.default.writeFileSync(e,`${JSON.stringify(t)}\n`,{mode:384})}function c(e){try{let t=JSON.parse(n.default.readFileSync(e,`utf8`));if(typeof t!=`object`||!t)return;let r=t;return{code:typeof r.code==`number`?r.code:null,signal:typeof r.signal==`string`?r.signal:null}}catch(t){t.code!==a&&process.emitWarning(`Could not read runner exit metadata ${e}: ${String(t)}`);return}}function l(e){for(let t of Object.values(e))try{n.default.unlinkSync(t)}catch(e){e.code!==a&&process.emitWarning(String(e))}}function u(e,i=require("url").pathToFileURL(__filename).href){let a=(0,r.fileURLToPath)(i),o=t.default.extname(a)===`.ts`,s=t.default.dirname(a);for(;;){let r=o?t.default.join(s,`dist`,`bin`,`${e}.mjs`):t.default.join(s,`bin`,`${e}.mjs`);if(n.default.existsSync(r))return n.default.realpathSync(r);let i=t.default.dirname(s);if(i===s)break;s=i}throw Error(`Cannot find bin/${e}.mjs by walking up from ${a}.`)}function d(e){return`exec ${f([process.execPath,u(`runnerHost`),e.spec,e.gate,e.exit])}`}function f(e){return e.map(e=>`'${e.replaceAll(`'`,`'"'"'`)}'`).join(` `)}exports.NO_TERMINAL_INPUT_ENV=i,exports.cleanupSupervisorFiles=l,exports.readExitMetadata=c,exports.runtimeEntry=u,exports.shellJoin=f,exports.supervisorCommand=d,exports.supervisorPaths=o,exports.writeCommandSpec=s;
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:url");const i={GIT_PAGER:`cat`,PAGER:`cat`,GIT_TERMINAL_PROMPT:`0`},a=`ENOENT`;function o(e,n){return{spec:t.default.join(e,`${n}.command.json`),gate:t.default.join(e,`${n}.gate`),exit:t.default.join(e,`${n}.exit.json`),logDone:t.default.join(e,`${n}.log.done`)}}function s(e,t){n.default.writeFileSync(e,`${JSON.stringify(t)}\n`,{mode:384})}function c(e){try{let t=JSON.parse(n.default.readFileSync(e,`utf8`));if(typeof t!=`object`||!t)return;let r=t;return{code:typeof r.code==`number`?r.code:null,signal:typeof r.signal==`string`?r.signal:null}}catch(t){t.code!==a&&process.emitWarning(`Could not read runner exit metadata ${e}: ${String(t)}`);return}}function l(e){for(let t of Object.values(e))try{n.default.unlinkSync(t)}catch(e){e.code!==a&&process.emitWarning(String(e))}}function u(e,i=require("url").pathToFileURL(__filename).href){let a=(0,r.fileURLToPath)(i),o=t.default.extname(a)===`.ts`,s=t.default.dirname(a);for(;;){let r=o?t.default.join(s,`dist`,`bin`,`${e}.mjs`):t.default.join(s,`bin`,`${e}.mjs`);if(n.default.existsSync(r))return n.default.realpathSync(r);let i=t.default.dirname(s);if(i===s)break;s=i}throw Error(`Cannot find bin/${e}.mjs by walking up from ${a}.`)}function d(e){return`exec ${f([process.execPath,u(`runnerHost`),e.spec,e.gate,e.exit])}`}function f(e){return e.map(e=>`'${e.replaceAll(`'`,`'"'"'`)}'`).join(` `)}const p=new Map;function m(e){let t=p.get(e);if(t)return t.references+=1,t;let r={watcher:void 0,waiters:new Map,references:1};try{r.watcher=n.default.watch(e,(e,t)=>{let n=t===null?void 0:String(t),i=n===void 0?[...r.waiters.values()]:[r.waiters.get(n)];for(let e of i)if(e)for(let t of e)t()}),r.watcher.on(`error`,()=>void 0)}catch{r.watcher=void 0}return p.set(e,r),r}function h(e,t){--t.references,!(t.references>0)&&(t.watcher?.close(),t.watcher=void 0,t.waiters.clear(),p.delete(e))}function g(e){let r=t.default.dirname(e),i=t.default.basename(e),a=m(r),o=!1,s=!1,c,l=()=>{!s&&n.default.existsSync(e)&&(s=!0,c?.())},u=a.waiters.get(i)??new Set;return u.add(l),a.waiters.set(i,u),l(),{async appeared(e){return s?!0:await new Promise(t=>{let n=setTimeout(()=>{c=void 0,l(),t(s)},e);c=()=>{clearTimeout(n),c=void 0,t(!0)}})},close(){o||(o=!0,c=void 0,u.delete(l),u.size===0&&a.waiters.delete(i),h(r,a))}}}exports.NO_TERMINAL_INPUT_ENV=i,exports.cleanupSupervisorFiles=l,exports.readExitMetadata=c,exports.runtimeEntry=u,exports.shellJoin=f,exports.supervisorCommand=d,exports.supervisorPaths=o,exports.watchForFile=g,exports.writeCommandSpec=s;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["path","fs","fileURLToPath"],"sources":["../../../src/services/runnerSupervisor/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/** What a supervisor needs to run one command. */\nexport interface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\n/** Exit status the supervisor records once its command is gone. */\nexport interface ExitMetadata {\n code: number | null;\n signal: NodeJS.Signals | null;\n}\n\n/** Sidecars a supervised run reads and writes beside its log. */\nexport interface SupervisorPaths {\n spec: string;\n gate: string;\n exit: string;\n logDone: string;\n}\n\n/**\n * Settings that make a terminal safe for a caller with no keyboard.\n *\n * Commands run inside an rmux pane, so their stdout is a real tty. Git and\n * friends take that as permission to open a pager, and `less` then waits at\n * `(END)` for a keypress nobody is there to send, so the run hangs until its\n * timeout. The same tty invites credential prompts, which hang the same way.\n * A conventional piped stdout disables both automatically; a pane has to be\n * told. A command that genuinely wants a pager can still ask for one, as in\n * `git -c core.pager=less log`.\n */\nexport const NO_TERMINAL_INPUT_ENV = {\n GIT_PAGER: 'cat',\n PAGER: 'cat',\n GIT_TERMINAL_PROMPT: '0',\n};\n\nconst SPEC_MODE = 0o600;\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\n\nexport function supervisorPaths(directory: string, id: string): SupervisorPaths {\n return {\n spec: path.join(directory, `${id}.command.json`),\n gate: path.join(directory, `${id}.gate`),\n exit: path.join(directory, `${id}.exit.json`),\n logDone: path.join(directory, `${id}.log.done`),\n };\n}\n\nexport function writeCommandSpec(target: string, spec: CommandSpec): void {\n fs.writeFileSync(target, `${JSON.stringify(spec)}\\n`, { mode: SPEC_MODE });\n}\n\nexport function readExitMetadata(target: string): ExitMetadata | undefined {\n try {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) return undefined;\n const metadata = value as Partial<ExitMetadata>;\n const code = typeof metadata.code === 'number' ? metadata.code : null;\n const signal = typeof metadata.signal === 'string' ? (metadata.signal as NodeJS.Signals) : null;\n return { code, signal };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n process.emitWarning(`Could not read runner exit metadata ${target}: ${String(error)}`);\n }\n return undefined;\n }\n}\n\nexport function cleanupSupervisorFiles(paths: SupervisorPaths): void {\n for (const target of Object.values(paths)) {\n try {\n fs.unlinkSync(target);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) process.emitWarning(String(error));\n }\n }\n}\n\n/**\n * Resolves an executable entry point by walking up to the `bin` directory that\n * ships beside this module. Source and published layouts nest services at\n * different depths. Source modules select built subprocess entries so Node can resolve their imports. A hardcoded depth stays silent\n * until the shell tries to exec it.\n */\nexport function runtimeEntry(name: 'runnerHost' | 'logSink', moduleUrl = import.meta.url): string {\n const modulePath = fileURLToPath(moduleUrl);\n const source = path.extname(modulePath) === '.ts';\n const extension = 'mjs';\n let directory = path.dirname(modulePath);\n\n while (true) {\n const entry = source\n ? path.join(directory, 'dist', 'bin', `${name}.${extension}`)\n : path.join(directory, 'bin', `${name}.${extension}`);\n if (fs.existsSync(entry)) return fs.realpathSync(entry);\n\n const parent = path.dirname(directory);\n if (parent === directory) break;\n directory = parent;\n }\n\n throw new Error(`Cannot find bin/${name}.${extension} by walking up from ${modulePath}.`);\n}\n\n/**\n * Replaces the shell rather than nesting under it, so the pid handed back is the\n * supervisor's and a group signal reaches it directly.\n */\nexport function supervisorCommand(paths: SupervisorPaths): string {\n return `exec ${shellJoin([process.execPath, runtimeEntry('runnerHost'), paths.spec, paths.gate, paths.exit])}`;\n}\n\nexport function shellJoin(values: readonly string[]): string {\n return values.map((value) => `'${value.replaceAll(\"'\", `'\"'\"'`)}'`).join(' ');\n}\n"],"mappings":"wKAoCA,MAAa,EAAwB,CACnC,UAAW,MACX,MAAO,MACP,oBAAqB,GACvB,EAGM,EAAuB,SAE7B,SAAgB,EAAgB,EAAmB,EAA6B,CAC9E,MAAO,CACL,KAAMA,EAAAA,QAAK,KAAK,EAAW,GAAG,EAAG,cAAc,EAC/C,KAAMA,EAAAA,QAAK,KAAK,EAAW,GAAG,EAAG,MAAM,EACvC,KAAMA,EAAAA,QAAK,KAAK,EAAW,GAAG,EAAG,WAAW,EAC5C,QAASA,EAAAA,QAAK,KAAK,EAAW,GAAG,EAAG,UAAU,CAChD,CACF,CAEA,SAAgB,EAAiB,EAAgB,EAAyB,CACxE,EAAA,QAAG,cAAc,EAAQ,GAAG,KAAK,UAAU,CAAI,EAAE,IAAK,CAAE,KAAM,GAAU,CAAC,CAC3E,CAEA,SAAgB,EAAiB,EAA0C,CACzE,GAAI,CACF,IAAM,EAAiB,KAAK,MAAMC,EAAAA,QAAG,aAAa,EAAQ,MAAM,CAAC,EACjE,GAAI,OAAO,GAAU,WAAY,EAAgB,OACjD,IAAM,EAAW,EAGjB,MAAO,CAAE,KAFI,OAAO,EAAS,MAAS,SAAW,EAAS,KAAO,KAElD,OADA,OAAO,EAAS,QAAW,SAAY,EAAS,OAA4B,IACrE,CACxB,OAAS,EAAO,CACT,EAAgC,OAAS,GAC5C,QAAQ,YAAY,uCAAuC,EAAO,IAAI,OAAO,CAAK,GAAG,EAEvF,MACF,CACF,CAEA,SAAgB,EAAuB,EAA8B,CACnE,IAAK,IAAM,KAAU,OAAO,OAAO,CAAK,EACtC,GAAI,CACF,EAAA,QAAG,WAAW,CAAM,CACtB,OAAS,EAAO,CACT,EAAgC,OAAS,GAAsB,QAAQ,YAAY,OAAO,CAAK,CAAC,CACvG,CAEJ,CAQA,SAAgB,EAAa,EAAgC,EAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,KAAqC,CAChG,IAAM,GAAA,EAAaC,EAAAA,cAAAA,CAAc,CAAS,EACpC,EAASF,EAAAA,QAAK,QAAQ,CAAU,IAAM,MAExC,EAAYA,EAAAA,QAAK,QAAQ,CAAU,EAEvC,OAAa,CACX,IAAM,EAAQ,EACVA,EAAAA,QAAK,KAAK,EAAW,OAAQ,MAAO,GAAG,EAAK,KAAc,EAC1DA,EAAAA,QAAK,KAAK,EAAW,MAAO,GAAG,EAAK,KAAc,EACtD,GAAIC,EAAAA,QAAG,WAAW,CAAK,EAAG,OAAOA,EAAAA,QAAG,aAAa,CAAK,EAEtD,IAAM,EAASD,EAAAA,QAAK,QAAQ,CAAS,EACrC,GAAI,IAAW,EAAW,MAC1B,EAAY,CACd,CAEA,MAAU,MAAM,mBAAmB,EAAK,0BAAmC,EAAW,EAAE,CAC1F,CAMA,SAAgB,EAAkB,EAAgC,CAChE,MAAO,QAAQ,EAAU,CAAC,QAAQ,SAAU,EAAa,YAAY,EAAG,EAAM,KAAM,EAAM,KAAM,EAAM,IAAI,CAAC,GAC7G,CAEA,SAAgB,EAAU,EAAmC,CAC3D,OAAO,EAAO,IAAK,GAAU,IAAI,EAAM,WAAW,IAAK,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAC9E"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["path","fs","fileURLToPath"],"sources":["../../../src/services/runnerSupervisor/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/** What a supervisor needs to run one command. */\nexport interface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\n/** Exit status the supervisor records once its command is gone. */\nexport interface ExitMetadata {\n code: number | null;\n signal: NodeJS.Signals | null;\n}\n\n/** Sidecars a supervised run reads and writes beside its log. */\nexport interface SupervisorPaths {\n spec: string;\n gate: string;\n exit: string;\n logDone: string;\n}\n\n/**\n * Settings that make a terminal safe for a caller with no keyboard.\n *\n * Commands run inside an rmux pane, so their stdout is a real tty. Git and\n * friends take that as permission to open a pager, and `less` then waits at\n * `(END)` for a keypress nobody is there to send, so the run hangs until its\n * timeout. The same tty invites credential prompts, which hang the same way.\n * A conventional piped stdout disables both automatically; a pane has to be\n * told. A command that genuinely wants a pager can still ask for one, as in\n * `git -c core.pager=less log`.\n */\nexport const NO_TERMINAL_INPUT_ENV = {\n GIT_PAGER: 'cat',\n PAGER: 'cat',\n GIT_TERMINAL_PROMPT: '0',\n};\n\nconst SPEC_MODE = 0o600;\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\n\nexport function supervisorPaths(directory: string, id: string): SupervisorPaths {\n return {\n spec: path.join(directory, `${id}.command.json`),\n gate: path.join(directory, `${id}.gate`),\n exit: path.join(directory, `${id}.exit.json`),\n logDone: path.join(directory, `${id}.log.done`),\n };\n}\n\nexport function writeCommandSpec(target: string, spec: CommandSpec): void {\n fs.writeFileSync(target, `${JSON.stringify(spec)}\\n`, { mode: SPEC_MODE });\n}\n\nexport function readExitMetadata(target: string): ExitMetadata | undefined {\n try {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) return undefined;\n const metadata = value as Partial<ExitMetadata>;\n const code = typeof metadata.code === 'number' ? metadata.code : null;\n const signal = typeof metadata.signal === 'string' ? (metadata.signal as NodeJS.Signals) : null;\n return { code, signal };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n process.emitWarning(`Could not read runner exit metadata ${target}: ${String(error)}`);\n }\n return undefined;\n }\n}\n\nexport function cleanupSupervisorFiles(paths: SupervisorPaths): void {\n for (const target of Object.values(paths)) {\n try {\n fs.unlinkSync(target);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) process.emitWarning(String(error));\n }\n }\n}\n\n/**\n * Resolves an executable entry point by walking up to the `bin` directory that\n * ships beside this module. Source and published layouts nest services at\n * different depths. Source modules select built subprocess entries so Node can resolve their imports. A hardcoded depth stays silent\n * until the shell tries to exec it.\n */\nexport function runtimeEntry(name: 'runnerHost' | 'logSink', moduleUrl = import.meta.url): string {\n const modulePath = fileURLToPath(moduleUrl);\n const source = path.extname(modulePath) === '.ts';\n const extension = 'mjs';\n let directory = path.dirname(modulePath);\n\n while (true) {\n const entry = source\n ? path.join(directory, 'dist', 'bin', `${name}.${extension}`)\n : path.join(directory, 'bin', `${name}.${extension}`);\n if (fs.existsSync(entry)) return fs.realpathSync(entry);\n\n const parent = path.dirname(directory);\n if (parent === directory) break;\n directory = parent;\n }\n\n throw new Error(`Cannot find bin/${name}.${extension} by walking up from ${modulePath}.`);\n}\n\n/**\n * Replaces the shell rather than nesting under it, so the pid handed back is the\n * supervisor's and a group signal reaches it directly.\n */\nexport function supervisorCommand(paths: SupervisorPaths): string {\n return `exec ${shellJoin([process.execPath, runtimeEntry('runnerHost'), paths.spec, paths.gate, paths.exit])}`;\n}\n\nexport function shellJoin(values: readonly string[]): string {\n return values.map((value) => `'${value.replaceAll(\"'\", `'\"'\"'`)}'`).join(' ');\n}\n\n/** A pending appearance of one supervisor sidecar, watched rather than polled for. */\nexport interface FileWatch {\n /** Resolves true once the file exists, or false when the slice elapses. */\n appeared(withinMs: number): Promise<boolean>;\n close(): void;\n}\n\ninterface DirectoryWatch {\n watcher: fs.FSWatcher | undefined;\n readonly waiters: Map<string, Set<() => void>>;\n references: number;\n}\n\n/**\n * One watcher per directory, shared by every waiter inside it.\n *\n * A session's runners share a runs directory, and `DOOMPI_LOG_DIR` makes every\n * session using it share one. A watcher per waited file would then put a watcher\n * per in-flight runner on the same directory, each woken by every other runner's\n * sidecar writes, so the watchers are pooled and reference counted instead.\n */\nconst directoryWatches = new Map<string, DirectoryWatch>();\n\nfunction acquireDirectoryWatch(directory: string): DirectoryWatch {\n const existing = directoryWatches.get(directory);\n if (existing) {\n existing.references += 1;\n return existing;\n }\n const entry: DirectoryWatch = { watcher: undefined, waiters: new Map(), references: 1 };\n try {\n entry.watcher = fs.watch(directory, (_event, changed) => {\n // A rename can arrive without a name, and then the only safe reading is\n // that any waiter in this directory might be the one it belongs to.\n const named = changed === null ? undefined : String(changed);\n const targets = named === undefined ? [...entry.waiters.values()] : [entry.waiters.get(named)];\n for (const listeners of targets) {\n if (!listeners) continue;\n for (const listener of listeners) listener();\n }\n });\n // A watcher that cannot report is a watcher whose slices time out, which the\n // caller's own probe already covers.\n entry.watcher.on('error', () => undefined);\n } catch {\n entry.watcher = undefined;\n }\n directoryWatches.set(directory, entry);\n return entry;\n}\n\nfunction releaseDirectoryWatch(directory: string, entry: DirectoryWatch): void {\n entry.references -= 1;\n if (entry.references > 0) return;\n entry.watcher?.close();\n entry.watcher = undefined;\n entry.waiters.clear();\n directoryWatches.delete(directory);\n}\n\n/**\n * Reports a supervisor sidecar landing on disk without polling for it.\n *\n * The sidecars are written once and never rewritten, so the directory's own\n * create event answers the only question a waiter has. Polling for them costs a\n * full poll interval on every run that finishes inside one, and the runs that\n * finish fastest are the ones that pay that tax most often.\n *\n * The watch is best effort: a platform or a directory that cannot deliver events\n * leaves every slice to time out, which is exactly the poll the caller already\n * runs as its fallback.\n */\nexport function watchForFile(filePath: string): FileWatch {\n const directory = path.dirname(filePath);\n const name = path.basename(filePath);\n const entry = acquireDirectoryWatch(directory);\n let released = false;\n let found = false;\n let notify: (() => void) | undefined;\n\n const settle = (): void => {\n if (found || !fs.existsSync(filePath)) return;\n found = true;\n notify?.();\n };\n const listeners = entry.waiters.get(name) ?? new Set<() => void>();\n listeners.add(settle);\n entry.waiters.set(name, listeners);\n\n // The file can land between opening the watcher and the first wait, and that\n // create event has nobody listening for it yet.\n settle();\n\n return {\n async appeared(withinMs: number): Promise<boolean> {\n if (found) return true;\n return await new Promise<boolean>((resolve) => {\n const timer = setTimeout(() => {\n notify = undefined;\n // The watcher is an optimisation, not the answer. A create event that\n // was coalesced, reported under the temporary name of a rename, or\n // never delivered at all must not outlive the slice it belongs to, so\n // every slice ends by asking the filesystem itself.\n settle();\n resolve(found);\n }, withinMs);\n // Deliberately left holding the event loop. A run still being waited on\n // is work, and a process that exits during the gap between two probes\n // drops the result its caller is awaiting.\n notify = () => {\n clearTimeout(timer);\n notify = undefined;\n resolve(true);\n };\n });\n },\n close(): void {\n if (released) return;\n released = true;\n notify = undefined;\n listeners.delete(settle);\n if (listeners.size === 0) entry.waiters.delete(name);\n releaseDirectoryWatch(directory, entry);\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"node:path";import t from"node:fs";import{fileURLToPath as n}from"node:url";const r={GIT_PAGER:`cat`,PAGER:`cat`,GIT_TERMINAL_PROMPT:`0`},i=`ENOENT`;function a(t,n){return{spec:e.join(t,`${n}.command.json`),gate:e.join(t,`${n}.gate`),exit:e.join(t,`${n}.exit.json`),logDone:e.join(t,`${n}.log.done`)}}function o(e,n){t.writeFileSync(e,`${JSON.stringify(n)}\n`,{mode:384})}function s(e){try{let n=JSON.parse(t.readFileSync(e,`utf8`));if(typeof n!=`object`||!n)return;let r=n;return{code:typeof r.code==`number`?r.code:null,signal:typeof r.signal==`string`?r.signal:null}}catch(t){t.code!==i&&process.emitWarning(`Could not read runner exit metadata ${e}: ${String(t)}`);return}}function c(e){for(let n of Object.values(e))try{t.unlinkSync(n)}catch(e){e.code!==i&&process.emitWarning(String(e))}}function l(r,i=import.meta.url){let a=n(i),o=e.extname(a)===`.ts`,s=e.dirname(a);for(;;){let n=o?e.join(s,`dist`,`bin`,`${r}.mjs`):e.join(s,`bin`,`${r}.mjs`);if(t.existsSync(n))return t.realpathSync(n);let i=e.dirname(s);if(i===s)break;s=i}throw Error(`Cannot find bin/${r}.mjs by walking up from ${a}.`)}function u(e){return`exec ${d([process.execPath,l(`runnerHost`),e.spec,e.gate,e.exit])}`}function d(e){return e.map(e=>`'${e.replaceAll(`'`,`'"'"'`)}'`).join(` `)}export{r as NO_TERMINAL_INPUT_ENV,c as cleanupSupervisorFiles,s as readExitMetadata,l as runtimeEntry,d as shellJoin,u as supervisorCommand,a as supervisorPaths,o as writeCommandSpec};
|
|
1
|
+
import e from"node:path";import t from"node:fs";import{fileURLToPath as n}from"node:url";const r={GIT_PAGER:`cat`,PAGER:`cat`,GIT_TERMINAL_PROMPT:`0`},i=`ENOENT`;function a(t,n){return{spec:e.join(t,`${n}.command.json`),gate:e.join(t,`${n}.gate`),exit:e.join(t,`${n}.exit.json`),logDone:e.join(t,`${n}.log.done`)}}function o(e,n){t.writeFileSync(e,`${JSON.stringify(n)}\n`,{mode:384})}function s(e){try{let n=JSON.parse(t.readFileSync(e,`utf8`));if(typeof n!=`object`||!n)return;let r=n;return{code:typeof r.code==`number`?r.code:null,signal:typeof r.signal==`string`?r.signal:null}}catch(t){t.code!==i&&process.emitWarning(`Could not read runner exit metadata ${e}: ${String(t)}`);return}}function c(e){for(let n of Object.values(e))try{t.unlinkSync(n)}catch(e){e.code!==i&&process.emitWarning(String(e))}}function l(r,i=import.meta.url){let a=n(i),o=e.extname(a)===`.ts`,s=e.dirname(a);for(;;){let n=o?e.join(s,`dist`,`bin`,`${r}.mjs`):e.join(s,`bin`,`${r}.mjs`);if(t.existsSync(n))return t.realpathSync(n);let i=e.dirname(s);if(i===s)break;s=i}throw Error(`Cannot find bin/${r}.mjs by walking up from ${a}.`)}function u(e){return`exec ${d([process.execPath,l(`runnerHost`),e.spec,e.gate,e.exit])}`}function d(e){return e.map(e=>`'${e.replaceAll(`'`,`'"'"'`)}'`).join(` `)}const f=new Map;function p(e){let n=f.get(e);if(n)return n.references+=1,n;let r={watcher:void 0,waiters:new Map,references:1};try{r.watcher=t.watch(e,(e,t)=>{let n=t===null?void 0:String(t),i=n===void 0?[...r.waiters.values()]:[r.waiters.get(n)];for(let e of i)if(e)for(let t of e)t()}),r.watcher.on(`error`,()=>void 0)}catch{r.watcher=void 0}return f.set(e,r),r}function m(e,t){--t.references,!(t.references>0)&&(t.watcher?.close(),t.watcher=void 0,t.waiters.clear(),f.delete(e))}function h(n){let r=e.dirname(n),i=e.basename(n),a=p(r),o=!1,s=!1,c,l=()=>{!s&&t.existsSync(n)&&(s=!0,c?.())},u=a.waiters.get(i)??new Set;return u.add(l),a.waiters.set(i,u),l(),{async appeared(e){return s?!0:await new Promise(t=>{let n=setTimeout(()=>{c=void 0,l(),t(s)},e);c=()=>{clearTimeout(n),c=void 0,t(!0)}})},close(){o||(o=!0,c=void 0,u.delete(l),u.size===0&&a.waiters.delete(i),m(r,a))}}}export{r as NO_TERMINAL_INPUT_ENV,c as cleanupSupervisorFiles,s as readExitMetadata,l as runtimeEntry,d as shellJoin,u as supervisorCommand,a as supervisorPaths,h as watchForFile,o as writeCommandSpec};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/runnerSupervisor/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/** What a supervisor needs to run one command. */\nexport interface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\n/** Exit status the supervisor records once its command is gone. */\nexport interface ExitMetadata {\n code: number | null;\n signal: NodeJS.Signals | null;\n}\n\n/** Sidecars a supervised run reads and writes beside its log. */\nexport interface SupervisorPaths {\n spec: string;\n gate: string;\n exit: string;\n logDone: string;\n}\n\n/**\n * Settings that make a terminal safe for a caller with no keyboard.\n *\n * Commands run inside an rmux pane, so their stdout is a real tty. Git and\n * friends take that as permission to open a pager, and `less` then waits at\n * `(END)` for a keypress nobody is there to send, so the run hangs until its\n * timeout. The same tty invites credential prompts, which hang the same way.\n * A conventional piped stdout disables both automatically; a pane has to be\n * told. A command that genuinely wants a pager can still ask for one, as in\n * `git -c core.pager=less log`.\n */\nexport const NO_TERMINAL_INPUT_ENV = {\n GIT_PAGER: 'cat',\n PAGER: 'cat',\n GIT_TERMINAL_PROMPT: '0',\n};\n\nconst SPEC_MODE = 0o600;\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\n\nexport function supervisorPaths(directory: string, id: string): SupervisorPaths {\n return {\n spec: path.join(directory, `${id}.command.json`),\n gate: path.join(directory, `${id}.gate`),\n exit: path.join(directory, `${id}.exit.json`),\n logDone: path.join(directory, `${id}.log.done`),\n };\n}\n\nexport function writeCommandSpec(target: string, spec: CommandSpec): void {\n fs.writeFileSync(target, `${JSON.stringify(spec)}\\n`, { mode: SPEC_MODE });\n}\n\nexport function readExitMetadata(target: string): ExitMetadata | undefined {\n try {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) return undefined;\n const metadata = value as Partial<ExitMetadata>;\n const code = typeof metadata.code === 'number' ? metadata.code : null;\n const signal = typeof metadata.signal === 'string' ? (metadata.signal as NodeJS.Signals) : null;\n return { code, signal };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n process.emitWarning(`Could not read runner exit metadata ${target}: ${String(error)}`);\n }\n return undefined;\n }\n}\n\nexport function cleanupSupervisorFiles(paths: SupervisorPaths): void {\n for (const target of Object.values(paths)) {\n try {\n fs.unlinkSync(target);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) process.emitWarning(String(error));\n }\n }\n}\n\n/**\n * Resolves an executable entry point by walking up to the `bin` directory that\n * ships beside this module. Source and published layouts nest services at\n * different depths. Source modules select built subprocess entries so Node can resolve their imports. A hardcoded depth stays silent\n * until the shell tries to exec it.\n */\nexport function runtimeEntry(name: 'runnerHost' | 'logSink', moduleUrl = import.meta.url): string {\n const modulePath = fileURLToPath(moduleUrl);\n const source = path.extname(modulePath) === '.ts';\n const extension = 'mjs';\n let directory = path.dirname(modulePath);\n\n while (true) {\n const entry = source\n ? path.join(directory, 'dist', 'bin', `${name}.${extension}`)\n : path.join(directory, 'bin', `${name}.${extension}`);\n if (fs.existsSync(entry)) return fs.realpathSync(entry);\n\n const parent = path.dirname(directory);\n if (parent === directory) break;\n directory = parent;\n }\n\n throw new Error(`Cannot find bin/${name}.${extension} by walking up from ${modulePath}.`);\n}\n\n/**\n * Replaces the shell rather than nesting under it, so the pid handed back is the\n * supervisor's and a group signal reaches it directly.\n */\nexport function supervisorCommand(paths: SupervisorPaths): string {\n return `exec ${shellJoin([process.execPath, runtimeEntry('runnerHost'), paths.spec, paths.gate, paths.exit])}`;\n}\n\nexport function shellJoin(values: readonly string[]): string {\n return values.map((value) => `'${value.replaceAll(\"'\", `'\"'\"'`)}'`).join(' ');\n}\n"],"mappings":"yFAoCA,MAAa,EAAwB,CACnC,UAAW,MACX,MAAO,MACP,oBAAqB,GACvB,EAGM,EAAuB,SAE7B,SAAgB,EAAgB,EAAmB,EAA6B,CAC9E,MAAO,CACL,KAAM,EAAK,KAAK,EAAW,GAAG,EAAG,cAAc,EAC/C,KAAM,EAAK,KAAK,EAAW,GAAG,EAAG,MAAM,EACvC,KAAM,EAAK,KAAK,EAAW,GAAG,EAAG,WAAW,EAC5C,QAAS,EAAK,KAAK,EAAW,GAAG,EAAG,UAAU,CAChD,CACF,CAEA,SAAgB,EAAiB,EAAgB,EAAyB,CACxE,EAAG,cAAc,EAAQ,GAAG,KAAK,UAAU,CAAI,EAAE,IAAK,CAAE,KAAM,GAAU,CAAC,CAC3E,CAEA,SAAgB,EAAiB,EAA0C,CACzE,GAAI,CACF,IAAM,EAAiB,KAAK,MAAM,EAAG,aAAa,EAAQ,MAAM,CAAC,EACjE,GAAI,OAAO,GAAU,WAAY,EAAgB,OACjD,IAAM,EAAW,EAGjB,MAAO,CAAE,KAFI,OAAO,EAAS,MAAS,SAAW,EAAS,KAAO,KAElD,OADA,OAAO,EAAS,QAAW,SAAY,EAAS,OAA4B,IACrE,CACxB,OAAS,EAAO,CACT,EAAgC,OAAS,GAC5C,QAAQ,YAAY,uCAAuC,EAAO,IAAI,OAAO,CAAK,GAAG,EAEvF,MACF,CACF,CAEA,SAAgB,EAAuB,EAA8B,CACnE,IAAK,IAAM,KAAU,OAAO,OAAO,CAAK,EACtC,GAAI,CACF,EAAG,WAAW,CAAM,CACtB,OAAS,EAAO,CACT,EAAgC,OAAS,GAAsB,QAAQ,YAAY,OAAO,CAAK,CAAC,CACvG,CAEJ,CAQA,SAAgB,EAAa,EAAgC,EAAY,YAAY,IAAa,CAChG,IAAM,EAAa,EAAc,CAAS,EACpC,EAAS,EAAK,QAAQ,CAAU,IAAM,MAExC,EAAY,EAAK,QAAQ,CAAU,EAEvC,OAAa,CACX,IAAM,EAAQ,EACV,EAAK,KAAK,EAAW,OAAQ,MAAO,GAAG,EAAK,KAAc,EAC1D,EAAK,KAAK,EAAW,MAAO,GAAG,EAAK,KAAc,EACtD,GAAI,EAAG,WAAW,CAAK,EAAG,OAAO,EAAG,aAAa,CAAK,EAEtD,IAAM,EAAS,EAAK,QAAQ,CAAS,EACrC,GAAI,IAAW,EAAW,MAC1B,EAAY,CACd,CAEA,MAAU,MAAM,mBAAmB,EAAK,0BAAmC,EAAW,EAAE,CAC1F,CAMA,SAAgB,EAAkB,EAAgC,CAChE,MAAO,QAAQ,EAAU,CAAC,QAAQ,SAAU,EAAa,YAAY,EAAG,EAAM,KAAM,EAAM,KAAM,EAAM,IAAI,CAAC,GAC7G,CAEA,SAAgB,EAAU,EAAmC,CAC3D,OAAO,EAAO,IAAK,GAAU,IAAI,EAAM,WAAW,IAAK,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAC9E"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/runnerSupervisor/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/** What a supervisor needs to run one command. */\nexport interface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\n/** Exit status the supervisor records once its command is gone. */\nexport interface ExitMetadata {\n code: number | null;\n signal: NodeJS.Signals | null;\n}\n\n/** Sidecars a supervised run reads and writes beside its log. */\nexport interface SupervisorPaths {\n spec: string;\n gate: string;\n exit: string;\n logDone: string;\n}\n\n/**\n * Settings that make a terminal safe for a caller with no keyboard.\n *\n * Commands run inside an rmux pane, so their stdout is a real tty. Git and\n * friends take that as permission to open a pager, and `less` then waits at\n * `(END)` for a keypress nobody is there to send, so the run hangs until its\n * timeout. The same tty invites credential prompts, which hang the same way.\n * A conventional piped stdout disables both automatically; a pane has to be\n * told. A command that genuinely wants a pager can still ask for one, as in\n * `git -c core.pager=less log`.\n */\nexport const NO_TERMINAL_INPUT_ENV = {\n GIT_PAGER: 'cat',\n PAGER: 'cat',\n GIT_TERMINAL_PROMPT: '0',\n};\n\nconst SPEC_MODE = 0o600;\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\n\nexport function supervisorPaths(directory: string, id: string): SupervisorPaths {\n return {\n spec: path.join(directory, `${id}.command.json`),\n gate: path.join(directory, `${id}.gate`),\n exit: path.join(directory, `${id}.exit.json`),\n logDone: path.join(directory, `${id}.log.done`),\n };\n}\n\nexport function writeCommandSpec(target: string, spec: CommandSpec): void {\n fs.writeFileSync(target, `${JSON.stringify(spec)}\\n`, { mode: SPEC_MODE });\n}\n\nexport function readExitMetadata(target: string): ExitMetadata | undefined {\n try {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) return undefined;\n const metadata = value as Partial<ExitMetadata>;\n const code = typeof metadata.code === 'number' ? metadata.code : null;\n const signal = typeof metadata.signal === 'string' ? (metadata.signal as NodeJS.Signals) : null;\n return { code, signal };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n process.emitWarning(`Could not read runner exit metadata ${target}: ${String(error)}`);\n }\n return undefined;\n }\n}\n\nexport function cleanupSupervisorFiles(paths: SupervisorPaths): void {\n for (const target of Object.values(paths)) {\n try {\n fs.unlinkSync(target);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) process.emitWarning(String(error));\n }\n }\n}\n\n/**\n * Resolves an executable entry point by walking up to the `bin` directory that\n * ships beside this module. Source and published layouts nest services at\n * different depths. Source modules select built subprocess entries so Node can resolve their imports. A hardcoded depth stays silent\n * until the shell tries to exec it.\n */\nexport function runtimeEntry(name: 'runnerHost' | 'logSink', moduleUrl = import.meta.url): string {\n const modulePath = fileURLToPath(moduleUrl);\n const source = path.extname(modulePath) === '.ts';\n const extension = 'mjs';\n let directory = path.dirname(modulePath);\n\n while (true) {\n const entry = source\n ? path.join(directory, 'dist', 'bin', `${name}.${extension}`)\n : path.join(directory, 'bin', `${name}.${extension}`);\n if (fs.existsSync(entry)) return fs.realpathSync(entry);\n\n const parent = path.dirname(directory);\n if (parent === directory) break;\n directory = parent;\n }\n\n throw new Error(`Cannot find bin/${name}.${extension} by walking up from ${modulePath}.`);\n}\n\n/**\n * Replaces the shell rather than nesting under it, so the pid handed back is the\n * supervisor's and a group signal reaches it directly.\n */\nexport function supervisorCommand(paths: SupervisorPaths): string {\n return `exec ${shellJoin([process.execPath, runtimeEntry('runnerHost'), paths.spec, paths.gate, paths.exit])}`;\n}\n\nexport function shellJoin(values: readonly string[]): string {\n return values.map((value) => `'${value.replaceAll(\"'\", `'\"'\"'`)}'`).join(' ');\n}\n\n/** A pending appearance of one supervisor sidecar, watched rather than polled for. */\nexport interface FileWatch {\n /** Resolves true once the file exists, or false when the slice elapses. */\n appeared(withinMs: number): Promise<boolean>;\n close(): void;\n}\n\ninterface DirectoryWatch {\n watcher: fs.FSWatcher | undefined;\n readonly waiters: Map<string, Set<() => void>>;\n references: number;\n}\n\n/**\n * One watcher per directory, shared by every waiter inside it.\n *\n * A session's runners share a runs directory, and `DOOMPI_LOG_DIR` makes every\n * session using it share one. A watcher per waited file would then put a watcher\n * per in-flight runner on the same directory, each woken by every other runner's\n * sidecar writes, so the watchers are pooled and reference counted instead.\n */\nconst directoryWatches = new Map<string, DirectoryWatch>();\n\nfunction acquireDirectoryWatch(directory: string): DirectoryWatch {\n const existing = directoryWatches.get(directory);\n if (existing) {\n existing.references += 1;\n return existing;\n }\n const entry: DirectoryWatch = { watcher: undefined, waiters: new Map(), references: 1 };\n try {\n entry.watcher = fs.watch(directory, (_event, changed) => {\n // A rename can arrive without a name, and then the only safe reading is\n // that any waiter in this directory might be the one it belongs to.\n const named = changed === null ? undefined : String(changed);\n const targets = named === undefined ? [...entry.waiters.values()] : [entry.waiters.get(named)];\n for (const listeners of targets) {\n if (!listeners) continue;\n for (const listener of listeners) listener();\n }\n });\n // A watcher that cannot report is a watcher whose slices time out, which the\n // caller's own probe already covers.\n entry.watcher.on('error', () => undefined);\n } catch {\n entry.watcher = undefined;\n }\n directoryWatches.set(directory, entry);\n return entry;\n}\n\nfunction releaseDirectoryWatch(directory: string, entry: DirectoryWatch): void {\n entry.references -= 1;\n if (entry.references > 0) return;\n entry.watcher?.close();\n entry.watcher = undefined;\n entry.waiters.clear();\n directoryWatches.delete(directory);\n}\n\n/**\n * Reports a supervisor sidecar landing on disk without polling for it.\n *\n * The sidecars are written once and never rewritten, so the directory's own\n * create event answers the only question a waiter has. Polling for them costs a\n * full poll interval on every run that finishes inside one, and the runs that\n * finish fastest are the ones that pay that tax most often.\n *\n * The watch is best effort: a platform or a directory that cannot deliver events\n * leaves every slice to time out, which is exactly the poll the caller already\n * runs as its fallback.\n */\nexport function watchForFile(filePath: string): FileWatch {\n const directory = path.dirname(filePath);\n const name = path.basename(filePath);\n const entry = acquireDirectoryWatch(directory);\n let released = false;\n let found = false;\n let notify: (() => void) | undefined;\n\n const settle = (): void => {\n if (found || !fs.existsSync(filePath)) return;\n found = true;\n notify?.();\n };\n const listeners = entry.waiters.get(name) ?? new Set<() => void>();\n listeners.add(settle);\n entry.waiters.set(name, listeners);\n\n // The file can land between opening the watcher and the first wait, and that\n // create event has nobody listening for it yet.\n settle();\n\n return {\n async appeared(withinMs: number): Promise<boolean> {\n if (found) return true;\n return await new Promise<boolean>((resolve) => {\n const timer = setTimeout(() => {\n notify = undefined;\n // The watcher is an optimisation, not the answer. A create event that\n // was coalesced, reported under the temporary name of a rename, or\n // never delivered at all must not outlive the slice it belongs to, so\n // every slice ends by asking the filesystem itself.\n settle();\n resolve(found);\n }, withinMs);\n // Deliberately left holding the event loop. A run still being waited on\n // is work, and a process that exits during the gap between two probes\n // drops the result its caller is awaiting.\n notify = () => {\n clearTimeout(timer);\n notify = undefined;\n resolve(true);\n };\n });\n },\n close(): void {\n if (released) return;\n released = true;\n notify = undefined;\n listeners.delete(settle);\n if (listeners.size === 0) entry.waiters.delete(name);\n releaseDirectoryWatch(directory, entry);\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../constants/spawner.cjs");let t=require("node:child_process");var n=class{spawn(n){let r=(0,t.spawn)(n.command,{cwd:n.cwd,env:n.env,detached:n.detached,shell:e.SHELL,stdio:[`ignore`,`pipe`,`pipe`]});return r.stdout?.setEncoding(`utf8`),r.stderr?.setEncoding(`utf8`),{pid:r.pid,onOutput(e){r.stdout?.on(`data`,t=>e(t,`stdout`)),r.stderr?.on(`data`,t=>e(t,`stderr`))},onExit(e){r.
|
|
1
|
+
const e=require("../../constants/spawner.cjs");let t=require("node:child_process");var n=class{spawn(n){let r=(0,t.spawn)(n.command,{cwd:n.cwd,env:n.env,detached:n.detached,shell:e.SHELL,stdio:[`ignore`,`pipe`,`pipe`]});return r.stdout?.setEncoding(`utf8`),r.stderr?.setEncoding(`utf8`),{pid:r.pid,onOutput(e){r.stdout?.on(`data`,t=>e(t,`stdout`)),r.stderr?.on(`data`,t=>e(t,`stderr`))},onExit(e){let t=!1,n,i=r=>{t||(t=!0,n&&clearTimeout(n),e(r))};r.once(`exit`,(e,t)=>{n=setTimeout(()=>i({code:e,signal:t}),250)}),r.once(`close`,(e,t)=>i({code:e,signal:t}))},onError(e){r.on(`error`,e)},kill(e){r.kill(e)},unref(){r.unref()}}}};exports.NodeSpawner=n;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["spawn","SHELL"],"sources":["../../../src/services/spawner/index.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\n\nimport { SHELL } from '../../constants/spawner';\nimport type { ExitResult, ISpawner, OutputStream, SpawnedProcess, SpawnRequest } from '../../types/spawner';\n\n/** Wraps `node:child_process.spawn`. */\nexport class NodeSpawner implements ISpawner {\n spawn(request: SpawnRequest): SpawnedProcess {\n const child = spawn(request.command, {\n cwd: request.cwd,\n env: request.env,\n detached: request.detached,\n shell: SHELL,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n child.stdout?.setEncoding('utf8');\n child.stderr?.setEncoding('utf8');\n\n return {\n pid: child.pid,\n onOutput(handler: (chunk: string, stream: OutputStream) => void): void {\n child.stdout?.on('data', (chunk: string) => handler(chunk, 'stdout'));\n child.stderr?.on('data', (chunk: string) => handler(chunk, 'stderr'));\n },\n onExit(handler: (result: ExitResult) => void): void {\n child.
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["spawn","SHELL"],"sources":["../../../src/services/spawner/index.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\n\nimport { EXIT_OUTPUT_DRAIN_MS, SHELL } from '../../constants/spawner';\nimport type { ExitResult, ISpawner, OutputStream, SpawnedProcess, SpawnRequest } from '../../types/spawner';\n\n/** Wraps `node:child_process.spawn`. */\nexport class NodeSpawner implements ISpawner {\n spawn(request: SpawnRequest): SpawnedProcess {\n const child = spawn(request.command, {\n cwd: request.cwd,\n env: request.env,\n detached: request.detached,\n shell: SHELL,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n child.stdout?.setEncoding('utf8');\n child.stderr?.setEncoding('utf8');\n\n return {\n pid: child.pid,\n onOutput(handler: (chunk: string, stream: OutputStream) => void): void {\n child.stdout?.on('data', (chunk: string) => handler(chunk, 'stdout'));\n child.stderr?.on('data', (chunk: string) => handler(chunk, 'stderr'));\n },\n onExit(handler: (result: ExitResult) => void): void {\n let finished = false;\n let drainTimer: ReturnType<typeof setTimeout> | undefined;\n const finish = (result: ExitResult): void => {\n if (finished) return;\n finished = true;\n if (drainTimer) clearTimeout(drainTimer);\n handler(result);\n };\n child.once('exit', (code, signal) => {\n // Descendants can keep inherited stdout/stderr open after the\n // supervised command exits. `close` would then wait for them too.\n drainTimer = setTimeout(() => finish({ code, signal }), EXIT_OUTPUT_DRAIN_MS);\n });\n child.once('close', (code, signal) => finish({ code, signal }));\n },\n onError(handler: (error: Error) => void): void {\n child.on('error', handler);\n },\n kill(signal?: NodeJS.Signals): void {\n child.kill(signal);\n },\n unref(): void {\n child.unref();\n },\n };\n }\n}\n"],"mappings":"mFAMA,IAAa,EAAb,KAA6C,CAC3C,MAAM,EAAuC,CAC3C,IAAM,GAAA,EAAQA,EAAAA,MAAAA,CAAM,EAAQ,QAAS,CACnC,IAAK,EAAQ,IACb,IAAK,EAAQ,IACb,SAAU,EAAQ,SAClB,MAAOC,EAAAA,MACP,MAAO,CAAC,SAAU,OAAQ,MAAM,CAClC,CAAC,EAKD,OAHA,EAAM,QAAQ,YAAY,MAAM,EAChC,EAAM,QAAQ,YAAY,MAAM,EAEzB,CACL,IAAK,EAAM,IACX,SAAS,EAA8D,CACrE,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAQ,EAAO,QAAQ,CAAC,EACpE,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAQ,EAAO,QAAQ,CAAC,CACtE,EACA,OAAO,EAA6C,CAClD,IAAI,EAAW,GACX,EACE,EAAU,GAA6B,CACvC,IACJ,EAAW,GACP,GAAY,aAAa,CAAU,EACvC,EAAQ,CAAM,EAChB,EACA,EAAM,KAAK,QAAS,EAAM,IAAW,CAGnC,EAAa,eAAiB,EAAO,CAAE,OAAM,QAAO,CAAC,EAAA,GAAuB,CAC9E,CAAC,EACD,EAAM,KAAK,SAAU,EAAM,IAAW,EAAO,CAAE,OAAM,QAAO,CAAC,CAAC,CAChE,EACA,QAAQ,EAAuC,CAC7C,EAAM,GAAG,QAAS,CAAO,CAC3B,EACA,KAAK,EAA+B,CAClC,EAAM,KAAK,CAAM,CACnB,EACA,OAAc,CACZ,EAAM,MAAM,CACd,CACF,CACF,CACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{SHELL as e}from"../../constants/spawner.mjs";import{spawn as t}from"node:child_process";var n=class{spawn(n){let r=t(n.command,{cwd:n.cwd,env:n.env,detached:n.detached,shell:e,stdio:[`ignore`,`pipe`,`pipe`]});return r.stdout?.setEncoding(`utf8`),r.stderr?.setEncoding(`utf8`),{pid:r.pid,onOutput(e){r.stdout?.on(`data`,t=>e(t,`stdout`)),r.stderr?.on(`data`,t=>e(t,`stderr`))},onExit(e){r.
|
|
1
|
+
import{SHELL as e}from"../../constants/spawner.mjs";import{spawn as t}from"node:child_process";var n=class{spawn(n){let r=t(n.command,{cwd:n.cwd,env:n.env,detached:n.detached,shell:e,stdio:[`ignore`,`pipe`,`pipe`]});return r.stdout?.setEncoding(`utf8`),r.stderr?.setEncoding(`utf8`),{pid:r.pid,onOutput(e){r.stdout?.on(`data`,t=>e(t,`stdout`)),r.stderr?.on(`data`,t=>e(t,`stderr`))},onExit(e){let t=!1,n,i=r=>{t||(t=!0,n&&clearTimeout(n),e(r))};r.once(`exit`,(e,t)=>{n=setTimeout(()=>i({code:e,signal:t}),250)}),r.once(`close`,(e,t)=>i({code:e,signal:t}))},onError(e){r.on(`error`,e)},kill(e){r.kill(e)},unref(){r.unref()}}}};export{n as NodeSpawner};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/spawner/index.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\n\nimport { SHELL } from '../../constants/spawner';\nimport type { ExitResult, ISpawner, OutputStream, SpawnedProcess, SpawnRequest } from '../../types/spawner';\n\n/** Wraps `node:child_process.spawn`. */\nexport class NodeSpawner implements ISpawner {\n spawn(request: SpawnRequest): SpawnedProcess {\n const child = spawn(request.command, {\n cwd: request.cwd,\n env: request.env,\n detached: request.detached,\n shell: SHELL,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n child.stdout?.setEncoding('utf8');\n child.stderr?.setEncoding('utf8');\n\n return {\n pid: child.pid,\n onOutput(handler: (chunk: string, stream: OutputStream) => void): void {\n child.stdout?.on('data', (chunk: string) => handler(chunk, 'stdout'));\n child.stderr?.on('data', (chunk: string) => handler(chunk, 'stderr'));\n },\n onExit(handler: (result: ExitResult) => void): void {\n child.
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/spawner/index.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\n\nimport { EXIT_OUTPUT_DRAIN_MS, SHELL } from '../../constants/spawner';\nimport type { ExitResult, ISpawner, OutputStream, SpawnedProcess, SpawnRequest } from '../../types/spawner';\n\n/** Wraps `node:child_process.spawn`. */\nexport class NodeSpawner implements ISpawner {\n spawn(request: SpawnRequest): SpawnedProcess {\n const child = spawn(request.command, {\n cwd: request.cwd,\n env: request.env,\n detached: request.detached,\n shell: SHELL,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n child.stdout?.setEncoding('utf8');\n child.stderr?.setEncoding('utf8');\n\n return {\n pid: child.pid,\n onOutput(handler: (chunk: string, stream: OutputStream) => void): void {\n child.stdout?.on('data', (chunk: string) => handler(chunk, 'stdout'));\n child.stderr?.on('data', (chunk: string) => handler(chunk, 'stderr'));\n },\n onExit(handler: (result: ExitResult) => void): void {\n let finished = false;\n let drainTimer: ReturnType<typeof setTimeout> | undefined;\n const finish = (result: ExitResult): void => {\n if (finished) return;\n finished = true;\n if (drainTimer) clearTimeout(drainTimer);\n handler(result);\n };\n child.once('exit', (code, signal) => {\n // Descendants can keep inherited stdout/stderr open after the\n // supervised command exits. `close` would then wait for them too.\n drainTimer = setTimeout(() => finish({ code, signal }), EXIT_OUTPUT_DRAIN_MS);\n });\n child.once('close', (code, signal) => finish({ code, signal }));\n },\n onError(handler: (error: Error) => void): void {\n child.on('error', handler);\n },\n kill(signal?: NodeJS.Signals): void {\n child.kill(signal);\n },\n unref(): void {\n child.unref();\n },\n };\n }\n}\n"],"mappings":"+FAMA,IAAa,EAAb,KAA6C,CAC3C,MAAM,EAAuC,CAC3C,IAAM,EAAQ,EAAM,EAAQ,QAAS,CACnC,IAAK,EAAQ,IACb,IAAK,EAAQ,IACb,SAAU,EAAQ,SAClB,MAAO,EACP,MAAO,CAAC,SAAU,OAAQ,MAAM,CAClC,CAAC,EAKD,OAHA,EAAM,QAAQ,YAAY,MAAM,EAChC,EAAM,QAAQ,YAAY,MAAM,EAEzB,CACL,IAAK,EAAM,IACX,SAAS,EAA8D,CACrE,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAQ,EAAO,QAAQ,CAAC,EACpE,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAQ,EAAO,QAAQ,CAAC,CACtE,EACA,OAAO,EAA6C,CAClD,IAAI,EAAW,GACX,EACE,EAAU,GAA6B,CACvC,IACJ,EAAW,GACP,GAAY,aAAa,CAAU,EACvC,EAAQ,CAAM,EAChB,EACA,EAAM,KAAK,QAAS,EAAM,IAAW,CAGnC,EAAa,eAAiB,EAAO,CAAE,OAAM,QAAO,CAAC,EAAA,GAAuB,CAC9E,CAAC,EACD,EAAM,KAAK,SAAU,EAAM,IAAW,EAAO,CAAE,OAAM,QAAO,CAAC,CAAC,CAChE,EACA,QAAQ,EAAuC,CAC7C,EAAM,GAAG,QAAS,CAAO,CAC3B,EACA,KAAK,EAA+B,CAClC,EAAM,KAAK,CAAM,CACnB,EACA,OAAc,CACZ,EAAM,MAAM,CACd,CACF,CACF,CACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e={lines:`lines`,grep:`grep`,ignoreCase:`ignoreCase`,contextLines:`contextLines`,from:`from`};exports.RUNNER_LOG_PARAMS=e;
|
|
1
|
+
require("@agimon-ai/doompi-core/web");const e={lines:`lines`,grep:`grep`,ignoreCase:`ignoreCase`,contextLines:`contextLines`,from:`from`};exports.RUNNER_LOG_PARAMS=e;
|
|
2
2
|
//# sourceMappingURL=webRunnerLog.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webRunnerLog.cjs","names":[],"sources":["../../src/types/webRunnerLog.ts"],"sourcesContent":["import { RUNNER_API_BASE_PATH, SESSION_QUERY_PARAM } from '../constants/webRunnerLog';\nimport type { LogSlice } from './logReader';\n\n/**\n * The runner log API, shared by this package's session-scoped routes and its\n * cockpit plugin. The two halves run in different processes, so the wire\n * vocabulary is declared here: `src/web` may reach `src/types` and nothing else on\n * the server side.\n *\n * The routes are mounted inside one session's own server, so they name a runner\n * and never a session or a path. The page addresses a session through the hub's\n * proxy parameter; the file a run writes to is read from its metadata record by\n * the server alone.\n */\n\n/** Where this package's API is mounted; the segment after /api/
|
|
1
|
+
{"version":3,"file":"webRunnerLog.cjs","names":[],"sources":["../../src/types/webRunnerLog.ts"],"sourcesContent":["import { sessionApiPath } from '@agimon-ai/doompi-core/web';\n\nimport { RUNNER_API_BASE_PATH, SESSION_QUERY_PARAM } from '../constants/webRunnerLog';\nimport type { LogSlice } from './logReader';\n\n/**\n * The runner log API, shared by this package's session-scoped routes and its\n * cockpit plugin. The two halves run in different processes, so the wire\n * vocabulary is declared here: `src/web` may reach `src/types` and nothing else on\n * the server side.\n *\n * The routes are mounted inside one session's own server, so they name a runner\n * and never a session or a path. The page addresses a session through the hub's\n * proxy parameter; the file a run writes to is read from its metadata record by\n * the server alone.\n */\n\n/** Where this package's API is mounted; the segment after /api/plugins/. */\n\n/** Query parameter the hub reads to pick which session server to proxy to. */\n\n/** One runner's log, relative to the API's own mount. */\nexport function runnerLogPath(runId: string): string {\n return `/runners/${encodeURIComponent(runId)}/log`;\n}\n\n/**\n * The absolute URL a page fetches for one runner's log, through the hub's proxy.\n *\n * The whole query is built here, session parameter included, so a caller never\n * has to know that the URL already carries one and never appends a second '?'.\n */\nexport function runnerLogUrl(sessionId: string, runId: string, params: RunnerLogQueryParams = {}): string {\n const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId });\n if (params.lines !== undefined) search.set(RUNNER_LOG_PARAMS.lines, String(params.lines));\n if (params.grep !== undefined && params.grep !== '') search.set(RUNNER_LOG_PARAMS.grep, params.grep);\n if (params.ignoreCase === true) search.set(RUNNER_LOG_PARAMS.ignoreCase, 'true');\n if (params.contextLines !== undefined) search.set(RUNNER_LOG_PARAMS.contextLines, String(params.contextLines));\n return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerLogPath(runId)}?${search.toString()}`;\n}\n\n/**\n * The absolute URL a page opens an EventSource on to follow one runner's log.\n *\n * `from` is the byte offset the page has already read, which the slice response\n * reports as `fileSize`. Passing it closes the gap between reading the tail and\n * opening the stream, so a line written in between is neither lost nor shown twice.\n */\nexport function runnerLogStreamUrl(sessionId: string, runId: string, from: number): string {\n const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId, [RUNNER_LOG_PARAMS.from]: String(from) });\n return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerLogPath(runId)}/stream?${search.toString()}`;\n}\n\n/** One runner's attached screen, relative to the API's own mount. */\nexport function runnerScreenPath(runId: string): string {\n return `/runners/${encodeURIComponent(runId)}/screen`;\n}\n\n/**\n * The URL a page opens an EventSource on to attach to an interactive runner.\n *\n * This is not the log. The log is scrubbed of cursor movement before it is\n * written, which is what makes it worth grepping and useless to a terminal.\n * The runner's sink keeps the unscrubbed bytes beside it, and this streams\n * those, so a real terminal emulator on the page can render them.\n */\nexport function runnerScreenStreamUrl(sessionId: string, runId: string, from = 0): string {\n const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId, [RUNNER_LOG_PARAMS.from]: String(from) });\n return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerScreenPath(runId)}/stream?${search.toString()}`;\n}\n\n/** The URL a page POSTs keystrokes to, for a runner that is waiting on input. */\nexport function runnerInputUrl(sessionId: string, runId: string): string {\n const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId });\n return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerScreenPath(runId)}/input?${search.toString()}`;\n}\n\n/**\n * One chunk of an attached pane's output.\n *\n * Base64 because these are terminal bytes, escape sequences and all, and a\n * server-sent event is a line-oriented text frame that would mangle them.\n */\nexport interface RunnerScreenEvent {\n chunk: string;\n /** Byte offset just past this chunk, so a reconnect resumes rather than replays. */\n offset?: number;\n /** Set once the runner exits, so the page can stop watching. */\n ended?: boolean;\n}\n\n/** The body of a keystroke POST. Text, not key codes: the pane wants bytes. */\nexport interface RunnerInputRequest {\n text: string;\n}\n\n/** The named SSE event carrying a RunnerScreenEvent payload. */\n\n/**\n * A log request as query parameters. `grep` is a literal substring, not a\n * regular expression, because that is what the reader matches; `lines` bounds\n * the answer to the last N lines of whatever survived the filter.\n */\nexport interface RunnerLogQueryParams {\n lines?: number;\n grep?: string;\n ignoreCase?: boolean;\n contextLines?: number;\n}\n\n/** The slice the log route answers with, plus which runner it came from. */\nexport interface RunnerLogResponse extends LogSlice {\n runId: string;\n /** True while the runner is still writing, so the page knows following is worth offering. */\n running: boolean;\n}\n\n/**\n * One server-sent event on the follow stream: the lines appended since the last\n * one. The stream never filters: a page that sets a query stops following and\n * reads a filtered slice instead, because grep with context is a whole-file\n * answer and cannot be assembled from a chunk at a time.\n */\nexport interface RunnerLogStreamEvent {\n /** Appended lines, in order; empty when the event only reports the runner ending. */\n lines: string[];\n /**\n * Byte offset just past the last line in this event.\n *\n * A follower that loses the stream resumes here. Without it the page could\n * only fall back to the offset its last slice reported, which every line\n * delivered since has already moved past, so a reconnect would replay them.\n */\n offset?: number;\n /** Set once the runner exits, so the page can stop following without polling the run list. */\n ended?: boolean;\n}\n\n/** The named SSE event carrying a RunnerLogStreamEvent payload. */\n\n/**\n * The named SSE event that carries nothing.\n *\n * A runner can be alive and silent for minutes, which on the wire is\n * indistinguishable from a connection that died. This is sent on a timer so\n * the socket keeps proving itself. Readers that only listen for `append`\n * ignore it, which is what makes it safe to add.\n */\n\n/** Query parameter names, shared so the page and the route cannot drift apart. */\nexport const RUNNER_LOG_PARAMS = {\n lines: 'lines',\n grep: 'grep',\n ignoreCase: 'ignoreCase',\n contextLines: 'contextLines',\n from: 'from',\n} as const;\n"],"mappings":"sCAsJA,MAAa,EAAoB,CAC/B,MAAO,QACP,KAAM,OACN,WAAY,aACZ,aAAc,eACd,KAAM,MACR"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e={lines:`lines`,grep:`grep`,ignoreCase:`ignoreCase`,contextLines:`contextLines`,from:`from`};export{e as RUNNER_LOG_PARAMS};
|
|
1
|
+
import"@agimon-ai/doompi-core/web";const e={lines:`lines`,grep:`grep`,ignoreCase:`ignoreCase`,contextLines:`contextLines`,from:`from`};export{e as RUNNER_LOG_PARAMS};
|
|
2
2
|
//# sourceMappingURL=webRunnerLog.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webRunnerLog.mjs","names":[],"sources":["../../src/types/webRunnerLog.ts"],"sourcesContent":["import { RUNNER_API_BASE_PATH, SESSION_QUERY_PARAM } from '../constants/webRunnerLog';\nimport type { LogSlice } from './logReader';\n\n/**\n * The runner log API, shared by this package's session-scoped routes and its\n * cockpit plugin. The two halves run in different processes, so the wire\n * vocabulary is declared here: `src/web` may reach `src/types` and nothing else on\n * the server side.\n *\n * The routes are mounted inside one session's own server, so they name a runner\n * and never a session or a path. The page addresses a session through the hub's\n * proxy parameter; the file a run writes to is read from its metadata record by\n * the server alone.\n */\n\n/** Where this package's API is mounted; the segment after /api/
|
|
1
|
+
{"version":3,"file":"webRunnerLog.mjs","names":[],"sources":["../../src/types/webRunnerLog.ts"],"sourcesContent":["import { sessionApiPath } from '@agimon-ai/doompi-core/web';\n\nimport { RUNNER_API_BASE_PATH, SESSION_QUERY_PARAM } from '../constants/webRunnerLog';\nimport type { LogSlice } from './logReader';\n\n/**\n * The runner log API, shared by this package's session-scoped routes and its\n * cockpit plugin. The two halves run in different processes, so the wire\n * vocabulary is declared here: `src/web` may reach `src/types` and nothing else on\n * the server side.\n *\n * The routes are mounted inside one session's own server, so they name a runner\n * and never a session or a path. The page addresses a session through the hub's\n * proxy parameter; the file a run writes to is read from its metadata record by\n * the server alone.\n */\n\n/** Where this package's API is mounted; the segment after /api/plugins/. */\n\n/** Query parameter the hub reads to pick which session server to proxy to. */\n\n/** One runner's log, relative to the API's own mount. */\nexport function runnerLogPath(runId: string): string {\n return `/runners/${encodeURIComponent(runId)}/log`;\n}\n\n/**\n * The absolute URL a page fetches for one runner's log, through the hub's proxy.\n *\n * The whole query is built here, session parameter included, so a caller never\n * has to know that the URL already carries one and never appends a second '?'.\n */\nexport function runnerLogUrl(sessionId: string, runId: string, params: RunnerLogQueryParams = {}): string {\n const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId });\n if (params.lines !== undefined) search.set(RUNNER_LOG_PARAMS.lines, String(params.lines));\n if (params.grep !== undefined && params.grep !== '') search.set(RUNNER_LOG_PARAMS.grep, params.grep);\n if (params.ignoreCase === true) search.set(RUNNER_LOG_PARAMS.ignoreCase, 'true');\n if (params.contextLines !== undefined) search.set(RUNNER_LOG_PARAMS.contextLines, String(params.contextLines));\n return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerLogPath(runId)}?${search.toString()}`;\n}\n\n/**\n * The absolute URL a page opens an EventSource on to follow one runner's log.\n *\n * `from` is the byte offset the page has already read, which the slice response\n * reports as `fileSize`. Passing it closes the gap between reading the tail and\n * opening the stream, so a line written in between is neither lost nor shown twice.\n */\nexport function runnerLogStreamUrl(sessionId: string, runId: string, from: number): string {\n const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId, [RUNNER_LOG_PARAMS.from]: String(from) });\n return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerLogPath(runId)}/stream?${search.toString()}`;\n}\n\n/** One runner's attached screen, relative to the API's own mount. */\nexport function runnerScreenPath(runId: string): string {\n return `/runners/${encodeURIComponent(runId)}/screen`;\n}\n\n/**\n * The URL a page opens an EventSource on to attach to an interactive runner.\n *\n * This is not the log. The log is scrubbed of cursor movement before it is\n * written, which is what makes it worth grepping and useless to a terminal.\n * The runner's sink keeps the unscrubbed bytes beside it, and this streams\n * those, so a real terminal emulator on the page can render them.\n */\nexport function runnerScreenStreamUrl(sessionId: string, runId: string, from = 0): string {\n const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId, [RUNNER_LOG_PARAMS.from]: String(from) });\n return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerScreenPath(runId)}/stream?${search.toString()}`;\n}\n\n/** The URL a page POSTs keystrokes to, for a runner that is waiting on input. */\nexport function runnerInputUrl(sessionId: string, runId: string): string {\n const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId });\n return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerScreenPath(runId)}/input?${search.toString()}`;\n}\n\n/**\n * One chunk of an attached pane's output.\n *\n * Base64 because these are terminal bytes, escape sequences and all, and a\n * server-sent event is a line-oriented text frame that would mangle them.\n */\nexport interface RunnerScreenEvent {\n chunk: string;\n /** Byte offset just past this chunk, so a reconnect resumes rather than replays. */\n offset?: number;\n /** Set once the runner exits, so the page can stop watching. */\n ended?: boolean;\n}\n\n/** The body of a keystroke POST. Text, not key codes: the pane wants bytes. */\nexport interface RunnerInputRequest {\n text: string;\n}\n\n/** The named SSE event carrying a RunnerScreenEvent payload. */\n\n/**\n * A log request as query parameters. `grep` is a literal substring, not a\n * regular expression, because that is what the reader matches; `lines` bounds\n * the answer to the last N lines of whatever survived the filter.\n */\nexport interface RunnerLogQueryParams {\n lines?: number;\n grep?: string;\n ignoreCase?: boolean;\n contextLines?: number;\n}\n\n/** The slice the log route answers with, plus which runner it came from. */\nexport interface RunnerLogResponse extends LogSlice {\n runId: string;\n /** True while the runner is still writing, so the page knows following is worth offering. */\n running: boolean;\n}\n\n/**\n * One server-sent event on the follow stream: the lines appended since the last\n * one. The stream never filters: a page that sets a query stops following and\n * reads a filtered slice instead, because grep with context is a whole-file\n * answer and cannot be assembled from a chunk at a time.\n */\nexport interface RunnerLogStreamEvent {\n /** Appended lines, in order; empty when the event only reports the runner ending. */\n lines: string[];\n /**\n * Byte offset just past the last line in this event.\n *\n * A follower that loses the stream resumes here. Without it the page could\n * only fall back to the offset its last slice reported, which every line\n * delivered since has already moved past, so a reconnect would replay them.\n */\n offset?: number;\n /** Set once the runner exits, so the page can stop following without polling the run list. */\n ended?: boolean;\n}\n\n/** The named SSE event carrying a RunnerLogStreamEvent payload. */\n\n/**\n * The named SSE event that carries nothing.\n *\n * A runner can be alive and silent for minutes, which on the wire is\n * indistinguishable from a connection that died. This is sent on a timer so\n * the socket keeps proving itself. Readers that only listen for `append`\n * ignore it, which is what makes it safe to add.\n */\n\n/** Query parameter names, shared so the page and the route cannot drift apart. */\nexport const RUNNER_LOG_PARAMS = {\n lines: 'lines',\n grep: 'grep',\n ignoreCase: 'ignoreCase',\n contextLines: 'contextLines',\n from: 'from',\n} as const;\n"],"mappings":"mCAsJA,MAAa,EAAoB,CAC/B,MAAO,QACP,KAAM,OACN,WAAY,aACZ,aAAc,eACd,KAAM,MACR"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-runner",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.72",
|
|
4
4
|
"description": "Supervised shell execution, background process control, and run logs for Pi coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -237,28 +237,28 @@
|
|
|
237
237
|
"registry": "https://registry.npmjs.org/"
|
|
238
238
|
},
|
|
239
239
|
"dependencies": {
|
|
240
|
-
"@agimon-ai/
|
|
240
|
+
"@agimon-ai/doompi-core": "0.0.1-alpha.72",
|
|
241
|
+
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.69",
|
|
242
|
+
"@agimon-ai/doompi-ui": "0.0.1-alpha.72",
|
|
243
|
+
"@agimon-ai/doompi-web-components": "0.0.1-alpha.30",
|
|
244
|
+
"@agimon-ai/doompi-web-security": "0.0.1-alpha.32",
|
|
245
|
+
"@agimon-ai/foundation-process-registry": "0.29.25",
|
|
241
246
|
"@deepseek-ai/cordis": "4.0.2",
|
|
242
247
|
"@rmux/sdk": "0.6.5",
|
|
243
248
|
"@xterm/headless": "6.0.0",
|
|
244
|
-
"hono": "4.13.
|
|
245
|
-
"typebox": "1.3.
|
|
246
|
-
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.68",
|
|
247
|
-
"@agimon-ai/doompi-ui": "0.0.1-alpha.71",
|
|
248
|
-
"@agimon-ai/doompi-core": "0.0.1-alpha.71",
|
|
249
|
-
"@agimon-ai/doompi-web-components": "0.0.1-alpha.29",
|
|
250
|
-
"@agimon-ai/doompi-web-security": "0.0.1-alpha.31"
|
|
249
|
+
"hono": "4.13.7",
|
|
250
|
+
"typebox": "1.3.30"
|
|
251
251
|
},
|
|
252
252
|
"devDependencies": {
|
|
253
253
|
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
254
254
|
"@earendil-works/pi-tui": "0.85.1",
|
|
255
255
|
"@tanstack/react-store": "0.11.1",
|
|
256
256
|
"@tanstack/store": "0.11.1",
|
|
257
|
-
"@types/node": "26.
|
|
258
|
-
"@types/react": "19.
|
|
257
|
+
"@types/node": "26.5.1",
|
|
258
|
+
"@types/react": "19.3.0",
|
|
259
259
|
"@vitest/coverage-v8": "5.0.0",
|
|
260
|
-
"react": "19.
|
|
261
|
-
"react-dom": "19.
|
|
260
|
+
"react": "19.3.0",
|
|
261
|
+
"react-dom": "19.3.0",
|
|
262
262
|
"tsdown": "0.23.0",
|
|
263
263
|
"typescript": "7.0.2",
|
|
264
264
|
"vitest": "5.0.0"
|
|
@@ -276,14 +276,14 @@
|
|
|
276
276
|
}
|
|
277
277
|
},
|
|
278
278
|
"optionalDependencies": {
|
|
279
|
-
"@agimon-ai/doompi-runner-rmux-darwin-arm64": "0.0.1-alpha.
|
|
280
|
-
"@agimon-ai/doompi-runner-rmux-darwin-x64": "0.0.1-alpha.
|
|
281
|
-
"@agimon-ai/doompi-runner-rmux-linux-arm64": "0.0.1-alpha.
|
|
282
|
-
"@agimon-ai/doompi-runner-rmux-linux-x64": "0.0.1-alpha.
|
|
283
|
-
"@agimon-ai/doompi-runner-rtk-darwin-arm64": "0.0.1-alpha.
|
|
284
|
-
"@agimon-ai/doompi-runner-rtk-darwin-x64": "0.0.1-alpha.
|
|
285
|
-
"@agimon-ai/doompi-runner-rtk-linux-arm64": "0.0.1-alpha.
|
|
286
|
-
"@agimon-ai/doompi-runner-rtk-linux-x64": "0.0.1-alpha.
|
|
279
|
+
"@agimon-ai/doompi-runner-rmux-darwin-arm64": "0.0.1-alpha.67",
|
|
280
|
+
"@agimon-ai/doompi-runner-rmux-darwin-x64": "0.0.1-alpha.67",
|
|
281
|
+
"@agimon-ai/doompi-runner-rmux-linux-arm64": "0.0.1-alpha.67",
|
|
282
|
+
"@agimon-ai/doompi-runner-rmux-linux-x64": "0.0.1-alpha.67",
|
|
283
|
+
"@agimon-ai/doompi-runner-rtk-darwin-arm64": "0.0.1-alpha.67",
|
|
284
|
+
"@agimon-ai/doompi-runner-rtk-darwin-x64": "0.0.1-alpha.67",
|
|
285
|
+
"@agimon-ai/doompi-runner-rtk-linux-arm64": "0.0.1-alpha.67",
|
|
286
|
+
"@agimon-ai/doompi-runner-rtk-linux-x64": "0.0.1-alpha.67"
|
|
287
287
|
},
|
|
288
288
|
"engines": {
|
|
289
289
|
"node": ">=22.19.0"
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export const RMUX_BINARY_ENV = 'DOOMPI_RMUX_BINARY';
|
|
2
2
|
export const POLL_MS = 100;
|
|
3
3
|
export const LOG_DRAIN_TIMEOUT_MS = 2_000;
|
|
4
|
+
/**
|
|
5
|
+
* Fallback interval for a missed log-completion event or a lost session.
|
|
6
|
+
* The retained pane lets the sink write its marker before session cleanup.
|
|
7
|
+
*/
|
|
4
8
|
export const LOG_DRAIN_POLL_MS = 25;
|
|
5
9
|
export const PANE_STATE_FORMAT = '#{pane_dead}:#{pane_dead_status}:#{session_name}';
|
|
10
|
+
export const PANE_PID_FORMAT = '#{pane_pid}';
|
|
6
11
|
export const STOP_GRACE_MS = 3_000;
|
|
7
12
|
export const STOP_CLOSE_TIMEOUT_MS = 1_000;
|
|
8
13
|
export const EXECUTABLE_MODE = 0o755;
|
package/src/constants/spawner.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sessionApiPath } from '@agimon-ai/doompi-core/web';
|
|
2
|
+
|
|
1
3
|
import { RUNNER_API_BASE_PATH, SESSION_QUERY_PARAM } from '../constants/webRunnerLog';
|
|
2
4
|
import type { LogSlice } from './logReader';
|
|
3
5
|
|
|
@@ -13,7 +15,7 @@ import type { LogSlice } from './logReader';
|
|
|
13
15
|
* the server alone.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
|
-
/** Where this package's API is mounted; the segment after /api/
|
|
18
|
+
/** Where this package's API is mounted; the segment after /api/plugins/. */
|
|
17
19
|
|
|
18
20
|
/** Query parameter the hub reads to pick which session server to proxy to. */
|
|
19
21
|
|
|
@@ -34,7 +36,7 @@ export function runnerLogUrl(sessionId: string, runId: string, params: RunnerLog
|
|
|
34
36
|
if (params.grep !== undefined && params.grep !== '') search.set(RUNNER_LOG_PARAMS.grep, params.grep);
|
|
35
37
|
if (params.ignoreCase === true) search.set(RUNNER_LOG_PARAMS.ignoreCase, 'true');
|
|
36
38
|
if (params.contextLines !== undefined) search.set(RUNNER_LOG_PARAMS.contextLines, String(params.contextLines));
|
|
37
|
-
return
|
|
39
|
+
return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerLogPath(runId)}?${search.toString()}`;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -46,7 +48,7 @@ export function runnerLogUrl(sessionId: string, runId: string, params: RunnerLog
|
|
|
46
48
|
*/
|
|
47
49
|
export function runnerLogStreamUrl(sessionId: string, runId: string, from: number): string {
|
|
48
50
|
const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId, [RUNNER_LOG_PARAMS.from]: String(from) });
|
|
49
|
-
return
|
|
51
|
+
return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerLogPath(runId)}/stream?${search.toString()}`;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/** One runner's attached screen, relative to the API's own mount. */
|
|
@@ -64,13 +66,13 @@ export function runnerScreenPath(runId: string): string {
|
|
|
64
66
|
*/
|
|
65
67
|
export function runnerScreenStreamUrl(sessionId: string, runId: string, from = 0): string {
|
|
66
68
|
const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId, [RUNNER_LOG_PARAMS.from]: String(from) });
|
|
67
|
-
return
|
|
69
|
+
return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerScreenPath(runId)}/stream?${search.toString()}`;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/** The URL a page POSTs keystrokes to, for a runner that is waiting on input. */
|
|
71
73
|
export function runnerInputUrl(sessionId: string, runId: string): string {
|
|
72
74
|
const search = new URLSearchParams({ [SESSION_QUERY_PARAM]: sessionId });
|
|
73
|
-
return
|
|
75
|
+
return `${sessionApiPath(sessionId)}/plugins/${RUNNER_API_BASE_PATH}${runnerScreenPath(runId)}/input?${search.toString()}`;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
/**
|