@agimon-ai/doompi-runner 0.0.1-alpha.37 → 0.0.1-alpha.39

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.
Files changed (131) hide show
  1. package/README.md +15 -4
  2. package/dist/sessionApi.cjs +1 -0
  3. package/dist/sessionApi.d.cts +2 -0
  4. package/dist/sessionApi.d.mts +2 -0
  5. package/dist/sessionApi.mjs +1 -0
  6. package/dist/src/adapters/LogTail/LogTail.cjs +3 -0
  7. package/dist/src/adapters/LogTail/LogTail.cjs.map +1 -0
  8. package/dist/src/adapters/LogTail/LogTail.mjs +3 -0
  9. package/dist/src/adapters/LogTail/LogTail.mjs.map +1 -0
  10. package/dist/src/adapters/PtyBackendChain/PtyBackendChain.cjs +2 -0
  11. package/dist/src/adapters/PtyBackendChain/PtyBackendChain.cjs.map +1 -0
  12. package/dist/src/adapters/PtyBackendChain/PtyBackendChain.mjs +2 -0
  13. package/dist/src/adapters/PtyBackendChain/PtyBackendChain.mjs.map +1 -0
  14. package/dist/src/adapters/RtkProcessor/RtkProcessor.cjs +1 -1
  15. package/dist/src/adapters/RtkProcessor/RtkProcessor.cjs.map +1 -1
  16. package/dist/src/adapters/RtkProcessor/RtkProcessor.d.cts +9 -0
  17. package/dist/src/adapters/RtkProcessor/RtkProcessor.d.cts.map +1 -1
  18. package/dist/src/adapters/RtkProcessor/RtkProcessor.d.mts +9 -0
  19. package/dist/src/adapters/RtkProcessor/RtkProcessor.d.mts.map +1 -1
  20. package/dist/src/adapters/RtkProcessor/RtkProcessor.mjs +1 -1
  21. package/dist/src/adapters/RtkProcessor/RtkProcessor.mjs.map +1 -1
  22. package/dist/src/adapters/RunnerPaths.cjs +1 -1
  23. package/dist/src/adapters/RunnerPaths.cjs.map +1 -1
  24. package/dist/src/adapters/RunnerPaths.d.cts.map +1 -1
  25. package/dist/src/adapters/RunnerPaths.d.mts.map +1 -1
  26. package/dist/src/adapters/RunnerPaths.mjs +1 -1
  27. package/dist/src/adapters/RunnerPaths.mjs.map +1 -1
  28. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.cjs +1 -1
  29. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.cjs.map +1 -1
  30. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.d.cts.map +1 -1
  31. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.d.mts.map +1 -1
  32. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.mjs +1 -1
  33. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.mjs.map +1 -1
  34. package/dist/src/adapters/TmuxBackend/TmuxBackend.cjs +2 -0
  35. package/dist/src/adapters/TmuxBackend/TmuxBackend.cjs.map +1 -0
  36. package/dist/src/adapters/TmuxBackend/TmuxBackend.mjs +2 -0
  37. package/dist/src/adapters/TmuxBackend/TmuxBackend.mjs.map +1 -0
  38. package/dist/src/adapters/TmuxBackend/TmuxClient.cjs +2 -0
  39. package/dist/src/adapters/TmuxBackend/TmuxClient.cjs.map +1 -0
  40. package/dist/src/adapters/TmuxBackend/TmuxClient.mjs +2 -0
  41. package/dist/src/adapters/TmuxBackend/TmuxClient.mjs.map +1 -0
  42. package/dist/src/adapters/pi/extension.cjs +2 -2
  43. package/dist/src/adapters/pi/extension.cjs.map +1 -1
  44. package/dist/src/adapters/pi/extension.d.cts.map +1 -1
  45. package/dist/src/adapters/pi/extension.d.mts.map +1 -1
  46. package/dist/src/adapters/pi/extension.mjs +2 -2
  47. package/dist/src/adapters/pi/extension.mjs.map +1 -1
  48. package/dist/src/adapters/runnerLogApi.cjs +2 -0
  49. package/dist/src/adapters/runnerLogApi.cjs.map +1 -0
  50. package/dist/src/adapters/runnerLogApi.d.cts +28 -0
  51. package/dist/src/adapters/runnerLogApi.d.cts.map +1 -0
  52. package/dist/src/adapters/runnerLogApi.d.mts +28 -0
  53. package/dist/src/adapters/runnerLogApi.d.mts.map +1 -0
  54. package/dist/src/adapters/runnerLogApi.mjs +2 -0
  55. package/dist/src/adapters/runnerLogApi.mjs.map +1 -0
  56. package/dist/src/adapters/webRunnerWatcher.cjs +2 -0
  57. package/dist/src/adapters/webRunnerWatcher.cjs.map +1 -0
  58. package/dist/src/adapters/webRunnerWatcher.d.cts +22 -0
  59. package/dist/src/adapters/webRunnerWatcher.d.cts.map +1 -0
  60. package/dist/src/adapters/webRunnerWatcher.d.mts +22 -0
  61. package/dist/src/adapters/webRunnerWatcher.d.mts.map +1 -0
  62. package/dist/src/adapters/webRunnerWatcher.mjs +2 -0
  63. package/dist/src/adapters/webRunnerWatcher.mjs.map +1 -0
  64. package/dist/src/adapters/webRunnersChannel.cjs +2 -0
  65. package/dist/src/adapters/webRunnersChannel.cjs.map +1 -0
  66. package/dist/src/adapters/webRunnersChannel.d.cts +15 -0
  67. package/dist/src/adapters/webRunnersChannel.d.cts.map +1 -0
  68. package/dist/src/adapters/webRunnersChannel.d.mts +15 -0
  69. package/dist/src/adapters/webRunnersChannel.d.mts.map +1 -0
  70. package/dist/src/adapters/webRunnersChannel.mjs +2 -0
  71. package/dist/src/adapters/webRunnersChannel.mjs.map +1 -0
  72. package/dist/src/commands/cli/cliApp.cjs +2 -2
  73. package/dist/src/commands/cli/cliApp.cjs.map +1 -1
  74. package/dist/src/commands/cli/cliApp.mjs +2 -2
  75. package/dist/src/commands/cli/cliApp.mjs.map +1 -1
  76. package/dist/src/container/index.cjs +1 -1
  77. package/dist/src/container/index.cjs.map +1 -1
  78. package/dist/src/container/index.d.cts.map +1 -1
  79. package/dist/src/container/index.d.mts.map +1 -1
  80. package/dist/src/container/index.mjs +1 -1
  81. package/dist/src/container/index.mjs.map +1 -1
  82. package/dist/src/services/runs/reconcile.cjs +1 -1
  83. package/dist/src/services/runs/reconcile.cjs.map +1 -1
  84. package/dist/src/services/runs/reconcile.d.cts.map +1 -1
  85. package/dist/src/services/runs/reconcile.d.mts.map +1 -1
  86. package/dist/src/services/runs/reconcile.mjs +1 -1
  87. package/dist/src/services/runs/reconcile.mjs.map +1 -1
  88. package/dist/src/services/runs/runnerRecord.cjs +2 -0
  89. package/dist/src/services/runs/runnerRecord.cjs.map +1 -0
  90. package/dist/src/services/runs/runnerRecord.mjs +2 -0
  91. package/dist/src/services/runs/runnerRecord.mjs.map +1 -0
  92. package/dist/src/services/runs/runnersCommand.cjs +2 -0
  93. package/dist/src/services/runs/runnersCommand.cjs.map +1 -0
  94. package/dist/src/services/runs/runnersCommand.mjs +2 -0
  95. package/dist/src/services/runs/runnersCommand.mjs.map +1 -0
  96. package/dist/src/services/webRunnerRuns.cjs +2 -0
  97. package/dist/src/services/webRunnerRuns.cjs.map +1 -0
  98. package/dist/src/services/webRunnerRuns.mjs +2 -0
  99. package/dist/src/services/webRunnerRuns.mjs.map +1 -0
  100. package/dist/src/types/logTail.d.cts +23 -0
  101. package/dist/src/types/logTail.d.cts.map +1 -0
  102. package/dist/src/types/logTail.d.mts +23 -0
  103. package/dist/src/types/logTail.d.mts.map +1 -0
  104. package/dist/src/types/webRunnerLog.cjs +2 -0
  105. package/dist/src/types/webRunnerLog.cjs.map +1 -0
  106. package/dist/src/types/webRunnerLog.mjs +2 -0
  107. package/dist/src/types/webRunnerLog.mjs.map +1 -0
  108. package/dist/src/types/webRunners.cjs +2 -0
  109. package/dist/src/types/webRunners.cjs.map +1 -0
  110. package/dist/src/types/webRunners.d.cts +38 -0
  111. package/dist/src/types/webRunners.d.cts.map +1 -0
  112. package/dist/src/types/webRunners.d.mts +38 -0
  113. package/dist/src/types/webRunners.d.mts.map +1 -0
  114. package/dist/src/types/webRunners.mjs +2 -0
  115. package/dist/src/types/webRunners.mjs.map +1 -0
  116. package/dist/webHub.cjs +1 -0
  117. package/dist/webHub.d.cts +2 -0
  118. package/dist/webHub.d.mts +2 -0
  119. package/dist/webHub.mjs +1 -0
  120. package/package.json +56 -18
  121. package/src/types/webRunnerLog.ts +95 -0
  122. package/src/types/webRunners.ts +39 -0
  123. package/web/BashToolMessage.tsx +156 -0
  124. package/web/RunnerLogPanel.tsx +263 -0
  125. package/web/RunnersActivitySection.tsx +107 -0
  126. package/web/bashToolFormat.ts +216 -0
  127. package/web/format.ts +42 -0
  128. package/web/index.ts +21 -0
  129. package/web/logApi.ts +81 -0
  130. package/web/runnersStore.ts +50 -0
  131. package/web/tsconfig.json +23 -0
@@ -1,2 +1,2 @@
1
- import{LOG_DIR_ENV as e}from"../types/config.mjs";import{inheritedSessionId as t}from"../services/runs/session.mjs";import n from"node:fs";import r from"node:path";import i from"node:os";import{execFileSync as a}from"node:child_process";const o=`doom-runner`,s=`runs`,c=`.json`,l=`utf8`,u=`ENOENT`;function d(e,t){try{let n=a(`git`,e,{cwd:t,encoding:l,stdio:[`ignore`,`pipe`,`ignore`]}).trim();return n.length>0?n:void 0}catch{return}}function f(e){try{return n.realpathSync(e)}catch{return e}}function p(e){let t=e.PI_CODING_AGENT_DIR?.trim();return t===`~`?i.homedir():t?.startsWith(`~/`)?r.join(i.homedir(),t.slice(2)):t?r.resolve(t):r.join(i.homedir(),`.pi`,`agent`)}function m(e){let t=n.statSync(e).mtimeMs;for(let i of n.readdirSync(e,{withFileTypes:!0})){let a=r.join(e,i.name);if(t=Math.max(t,n.statSync(a).mtimeMs),i.isDirectory())for(let e of n.readdirSync(a))t=Math.max(t,n.statSync(r.join(a,e)).mtimeMs)}return t}async function h(e){let t=(await n.promises.stat(e)).mtimeMs;for(let i of await n.promises.readdir(e,{withFileTypes:!0})){let a=r.join(e,i.name);if(t=Math.max(t,(await n.promises.stat(a)).mtimeMs),i.isDirectory())for(let e of await n.promises.readdir(a))t=Math.max(t,(await n.promises.stat(r.join(a,e))).mtimeMs)}return t}function g(e){let t;try{t=n.readdirSync(e)}catch{return!1}for(let i of t)if(i.endsWith(c))try{let t=JSON.parse(n.readFileSync(r.join(e,i),l)).hostPid;if(typeof t!=`number`)continue;return process.kill(t,0),!0}catch{}return!1}async function _(e){let t;try{t=await n.promises.readdir(e)}catch{return!1}for(let i of t)if(i.endsWith(c))try{let t=JSON.parse(await n.promises.readFile(r.join(e,i),l)).hostPid;if(typeof t!=`number`)continue;return process.kill(t,0),!0}catch{}return!1}function v(e){if(typeof e!=`object`||!e)return;let t=e;if(t.state!==`completed`||typeof t.exit?.finishedAt!=`string`)return;let n=Date.parse(t.exit.finishedAt);return Number.isFinite(n)?n:void 0}var y=class{adoptedSessionId;repository;repositoryPath(){return this.repository??=f(d([`rev-parse`,`--show-toplevel`],process.cwd())??process.cwd()),this.repository}setSessionId(e){let t=e.trim();if(!t)throw Error(`doom-runner session id cannot be blank`);this.adoptedSessionId=t}logDirectory(t){let n=process.env[e]?.trim();return n?r.resolve(n):r.join(this.sessionDirectory(t),`logs`)}stateDirectory(t){let n=process.env[e]?.trim();return n?r.join(r.resolve(n),`..`,s):r.join(this.sessionDirectory(t),s)}logPathFor(e,t){return r.join(this.logDirectory(t),`${e}.log`)}rotatedLogPathFor(e,t){return`${this.logPathFor(e,t)}.1`}statePathFor(e,t){return r.join(this.stateDirectory(t),`${e}${c}`)}ensureDirectories(e){n.mkdirSync(this.logDirectory(e),{recursive:!0}),n.mkdirSync(this.stateDirectory(e),{recursive:!0})}sweepHistory(e,t=Date.now()){let i={removed:[],errors:[]};if(process.env.DOOM_RUNNER_LOG_DIR?.trim())return this.sweepStateDirectory(this.stateDirectory(),this.sessionId(),e,t,i),i;let a=this.storeDirectory(),o;try{o=n.readdirSync(a)}catch(e){return e.code===u||i.errors.push(`Could not read runner history directory ${a}: ${String(e)}`),i}let s=this.currentSessionId();for(let n of o)n!==s&&this.sweepSessionDirectory(r.join(a,n),e,t,i);return i}async sweepHistoryAsync(e,t=Date.now()){let i={removed:[],errors:[]};if(process.env.DOOM_RUNNER_LOG_DIR?.trim())return await this.sweepStateDirectoryAsync(this.stateDirectory(),this.sessionId(),e,t,i),i;let a=this.storeDirectory(),o;try{o=await n.promises.readdir(a)}catch(e){return e.code===u||i.errors.push(`Could not read runner history directory ${a}: ${String(e)}`),i}let s=this.currentSessionId();for(let n of o)n!==s&&await this.sweepSessionDirectoryAsync(r.join(a,n),e,t,i);return i}legacyDirectory(){let e=d([`rev-parse`,`--path-format=absolute`,`--git-common-dir`],process.cwd());return e?r.join(e,o):void 0}removeLegacyStore(){let e=this.legacyDirectory();if(!(!e||r.basename(e)!==o||!n.existsSync(e)))return n.rmSync(e,{recursive:!0,force:!0}),e}currentSessionId(){try{return this.sessionId()}catch{return}}sweepSessionDirectory(e,t,i,a){try{if(i-m(e)<=t||g(r.join(e,s)))return;n.rmSync(e,{recursive:!0,force:!0}),a.removed.push(e)}catch(t){t.code!==u&&a.errors.push(`Could not sweep runner session ${e}: ${String(t)}`)}}async sweepSessionDirectoryAsync(e,t,i,a){try{if(i-await h(e)<=t||await _(r.join(e,s)))return;await n.promises.rm(e,{recursive:!0,force:!0}),a.removed.push(e)}catch(t){t.code!==u&&a.errors.push(`Could not sweep runner session ${e}: ${String(t)}`)}}sessionId(e){let n=e?.trim()||this.adoptedSessionId||t();if(!n)throw Error(`PI_SESSION_ID is required for doom-runner storage`);return n}storeDirectory(){let t=process.env[e]?.trim();return t?r.dirname(r.resolve(t)):r.join(p(process.env),o)}sessionDirectory(e){return r.join(this.storeDirectory(),this.sessionId(e))}sweepStateDirectory(e,t,i,a,o){let s;try{s=n.readdirSync(e)}catch(t){t.code!==u&&o.errors.push(`Could not read runner state directory ${e}: ${String(t)}`);return}for(let u of s){if(!u.endsWith(c))continue;let s=r.join(e,u);try{let e=v(JSON.parse(n.readFileSync(s,l)));if(e===void 0||a-e<=i)continue;let r=u.slice(0,-5);this.removeHistoryFiles(t,r,s,o)}catch(e){o.errors.push(`Could not sweep runner metadata ${s}: ${String(e)}`)}}}async sweepStateDirectoryAsync(e,t,i,a,o){let s;try{s=await n.promises.readdir(e)}catch(t){t.code!==u&&o.errors.push(`Could not read runner state directory ${e}: ${String(t)}`);return}for(let u of s){if(!u.endsWith(c))continue;let s=r.join(e,u);try{let e=v(JSON.parse(await n.promises.readFile(s,l)));if(e===void 0||a-e<=i)continue;let r=u.slice(0,-5);await this.removeHistoryFilesAsync(t,r,s,o)}catch(e){o.errors.push(`Could not sweep runner metadata ${s}: ${String(e)}`)}}}removeHistoryFiles(e,t,r,i){let a=[r,this.logPathFor(t,e),this.rotatedLogPathFor(t,e)];for(let e of a)try{n.unlinkSync(e),i.removed.push(e)}catch(t){t.code!==u&&i.errors.push(`Could not remove runner history ${e}: ${String(t)}`)}}async removeHistoryFilesAsync(e,t,r,i){let a=[r,this.logPathFor(t,e),this.rotatedLogPathFor(t,e)];for(let e of a)try{await n.promises.unlink(e),i.removed.push(e)}catch(t){t.code!==u&&i.errors.push(`Could not remove runner history ${e}: ${String(t)}`)}}};export{y as RunnerPaths};
1
+ import{LOG_DIR_ENV as e}from"../types/config.mjs";import{inheritedSessionId as t}from"../services/runs/session.mjs";import n from"node:fs";import r from"node:path";import i from"node:os";import{execFileSync as a}from"node:child_process";const o=`doom-runner`,s=`runs`,c=`.json`,l=`utf8`,u=`ENOENT`;function d(e,t){try{let n=a(`git`,e,{cwd:t,encoding:l,stdio:[`ignore`,`pipe`,`ignore`]}).trim();return n.length>0?n:void 0}catch{return}}function f(e){try{return n.realpathSync(e)}catch{return e}}function p(e){let t=e.PI_CODING_AGENT_DIR?.trim();return t===`~`?i.homedir():t?.startsWith(`~/`)?r.join(i.homedir(),t.slice(2)):t?r.resolve(t):r.join(i.homedir(),`.pi`,`agent`)}function m(e){let t=n.statSync(e).mtimeMs;for(let i of n.readdirSync(e,{withFileTypes:!0})){let a=r.join(e,i.name);if(t=Math.max(t,n.statSync(a).mtimeMs),i.isDirectory())for(let e of n.readdirSync(a))t=Math.max(t,n.statSync(r.join(a,e)).mtimeMs)}return t}async function h(e){let t=(await n.promises.stat(e)).mtimeMs;for(let i of await n.promises.readdir(e,{withFileTypes:!0})){let a=r.join(e,i.name);if(t=Math.max(t,(await n.promises.stat(a)).mtimeMs),i.isDirectory())for(let e of await n.promises.readdir(a))t=Math.max(t,(await n.promises.stat(r.join(a,e))).mtimeMs)}return t}function g(e){let t;try{t=n.readdirSync(e)}catch{return!1}for(let i of t)if(i.endsWith(c))try{let t=JSON.parse(n.readFileSync(r.join(e,i),l)).hostPid;if(typeof t!=`number`)continue;return process.kill(t,0),!0}catch{}return!1}async function _(e){let t;try{t=await n.promises.readdir(e)}catch{return!1}for(let i of t)if(i.endsWith(c))try{let t=JSON.parse(await n.promises.readFile(r.join(e,i),l)).hostPid;if(typeof t!=`number`)continue;return process.kill(t,0),!0}catch{}return!1}function v(e){if(typeof e!=`object`||!e)return;let t=e;if(t.state!==`completed`||typeof t.exit?.finishedAt!=`string`)return;let n=Date.parse(t.exit.finishedAt);return Number.isFinite(n)?n:void 0}function y(e){return r.join(p(e),o)}var b=class{adoptedSessionId;repository;repositoryPath(){return this.repository??=f(d([`rev-parse`,`--show-toplevel`],process.cwd())??process.cwd()),this.repository}setSessionId(e){let t=e.trim();if(!t)throw Error(`doom-runner session id cannot be blank`);this.adoptedSessionId=t}logDirectory(t){let n=process.env[e]?.trim();return n?r.resolve(n):r.join(this.sessionDirectory(t),`logs`)}stateDirectory(t){let n=process.env[e]?.trim();return n?r.join(r.resolve(n),`..`,s):r.join(this.sessionDirectory(t),s)}logPathFor(e,t){return r.join(this.logDirectory(t),`${e}.log`)}rotatedLogPathFor(e,t){return`${this.logPathFor(e,t)}.1`}statePathFor(e,t){return r.join(this.stateDirectory(t),`${e}${c}`)}ensureDirectories(e){n.mkdirSync(this.logDirectory(e),{recursive:!0}),n.mkdirSync(this.stateDirectory(e),{recursive:!0})}sweepHistory(e,t=Date.now()){let i={removed:[],errors:[]};if(process.env.DOOM_RUNNER_LOG_DIR?.trim())return this.sweepStateDirectory(this.stateDirectory(),this.sessionId(),e,t,i),i;let a=this.storeDirectory(),o;try{o=n.readdirSync(a)}catch(e){return e.code===u||i.errors.push(`Could not read runner history directory ${a}: ${String(e)}`),i}let s=this.currentSessionId();for(let n of o)n!==s&&this.sweepSessionDirectory(r.join(a,n),e,t,i);return i}async sweepHistoryAsync(e,t=Date.now()){let i={removed:[],errors:[]};if(process.env.DOOM_RUNNER_LOG_DIR?.trim())return await this.sweepStateDirectoryAsync(this.stateDirectory(),this.sessionId(),e,t,i),i;let a=this.storeDirectory(),o;try{o=await n.promises.readdir(a)}catch(e){return e.code===u||i.errors.push(`Could not read runner history directory ${a}: ${String(e)}`),i}let s=this.currentSessionId();for(let n of o)n!==s&&await this.sweepSessionDirectoryAsync(r.join(a,n),e,t,i);return i}legacyDirectory(){let e=d([`rev-parse`,`--path-format=absolute`,`--git-common-dir`],process.cwd());return e?r.join(e,o):void 0}removeLegacyStore(){let e=this.legacyDirectory();if(!(!e||r.basename(e)!==o||!n.existsSync(e)))return n.rmSync(e,{recursive:!0,force:!0}),e}currentSessionId(){try{return this.sessionId()}catch{return}}sweepSessionDirectory(e,t,i,a){try{if(i-m(e)<=t||g(r.join(e,s)))return;n.rmSync(e,{recursive:!0,force:!0}),a.removed.push(e)}catch(t){t.code!==u&&a.errors.push(`Could not sweep runner session ${e}: ${String(t)}`)}}async sweepSessionDirectoryAsync(e,t,i,a){try{if(i-await h(e)<=t||await _(r.join(e,s)))return;await n.promises.rm(e,{recursive:!0,force:!0}),a.removed.push(e)}catch(t){t.code!==u&&a.errors.push(`Could not sweep runner session ${e}: ${String(t)}`)}}sessionId(e){let n=e?.trim()||this.adoptedSessionId||t();if(!n)throw Error(`PI_SESSION_ID is required for doom-runner storage`);return n}storeDirectory(){let t=process.env[e]?.trim();return t?r.dirname(r.resolve(t)):y(process.env)}sessionDirectory(e){return r.join(this.storeDirectory(),this.sessionId(e))}sweepStateDirectory(e,t,i,a,o){let s;try{s=n.readdirSync(e)}catch(t){t.code!==u&&o.errors.push(`Could not read runner state directory ${e}: ${String(t)}`);return}for(let u of s){if(!u.endsWith(c))continue;let s=r.join(e,u);try{let e=v(JSON.parse(n.readFileSync(s,l)));if(e===void 0||a-e<=i)continue;let r=u.slice(0,-5);this.removeHistoryFiles(t,r,s,o)}catch(e){o.errors.push(`Could not sweep runner metadata ${s}: ${String(e)}`)}}}async sweepStateDirectoryAsync(e,t,i,a,o){let s;try{s=await n.promises.readdir(e)}catch(t){t.code!==u&&o.errors.push(`Could not read runner state directory ${e}: ${String(t)}`);return}for(let u of s){if(!u.endsWith(c))continue;let s=r.join(e,u);try{let e=v(JSON.parse(await n.promises.readFile(s,l)));if(e===void 0||a-e<=i)continue;let r=u.slice(0,-5);await this.removeHistoryFilesAsync(t,r,s,o)}catch(e){o.errors.push(`Could not sweep runner metadata ${s}: ${String(e)}`)}}}removeHistoryFiles(e,t,r,i){let a=[r,this.logPathFor(t,e),this.rotatedLogPathFor(t,e)];for(let e of a)try{n.unlinkSync(e),i.removed.push(e)}catch(t){t.code!==u&&i.errors.push(`Could not remove runner history ${e}: ${String(t)}`)}}async removeHistoryFilesAsync(e,t,r,i){let a=[r,this.logPathFor(t,e),this.rotatedLogPathFor(t,e)];for(let e of a)try{await n.promises.unlink(e),i.removed.push(e)}catch(t){t.code!==u&&i.errors.push(`Could not remove runner history ${e}: ${String(t)}`)}}};export{b as RunnerPaths,y as resolveRunnerStoreDirectory};
2
2
  //# sourceMappingURL=RunnerPaths.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"RunnerPaths.mjs","names":[],"sources":["../../../src/adapters/RunnerPaths.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport type { IRunnerPaths, LogSweepResult } from '../services/RunnerPaths/types.ts';\nimport { inheritedSessionId } from '../services/runs/session.ts';\nimport { LOG_DIR_ENV } from '../types/config.ts';\n\nconst STORE_DIR_NAME = 'doom-runner';\nconst DEFAULT_CONFIG_DIR_NAME = '.pi';\nconst AGENT_DIR_NAME = 'agent';\nconst LOG_DIR_NAME = 'logs';\nconst STATE_DIR_NAME = 'runs';\nconst LOG_EXTENSION = '.log';\nconst STATE_EXTENSION = '.json';\nconst ROTATED_SUFFIX = '.1';\nconst PI_CODING_AGENT_DIR_ENV = 'PI_CODING_AGENT_DIR';\nconst HOME_ALIAS = '~';\nconst HOME_ALIAS_PREFIX = '~/';\nconst FILE_ENCODING = 'utf8';\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\n\nfunction git(args: string[], cwd: string): string | undefined {\n try {\n const output = execFileSync('git', args, { cwd, encoding: FILE_ENCODING, stdio: ['ignore', 'pipe', 'ignore'] });\n const trimmed = output.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction realPath(target: string): string {\n try {\n return fs.realpathSync(target);\n } catch {\n return target;\n }\n}\n\nfunction resolveAgentDirectory(env: NodeJS.ProcessEnv): string {\n const configured = env[PI_CODING_AGENT_DIR_ENV]?.trim();\n if (configured === HOME_ALIAS) return os.homedir();\n if (configured?.startsWith(HOME_ALIAS_PREFIX)) {\n return path.join(os.homedir(), configured.slice(HOME_ALIAS_PREFIX.length));\n }\n return configured ? path.resolve(configured) : path.join(os.homedir(), DEFAULT_CONFIG_DIR_NAME, AGENT_DIR_NAME);\n}\n\n/**\n * Newest mtime anywhere one level inside the session, falling back to the\n * directory itself. A session still being written to therefore always looks\n * recent, whatever state its individual records are in.\n */\nfunction newestEntryTime(directory: string): number {\n let newest = fs.statSync(directory).mtimeMs;\n for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {\n const target = path.join(directory, entry.name);\n newest = Math.max(newest, fs.statSync(target).mtimeMs);\n if (!entry.isDirectory()) continue;\n for (const child of fs.readdirSync(target)) {\n newest = Math.max(newest, fs.statSync(path.join(target, child)).mtimeMs);\n }\n }\n return newest;\n}\n\nasync function newestEntryTimeAsync(directory: string): Promise<number> {\n let newest = (await fs.promises.stat(directory)).mtimeMs;\n for (const entry of await fs.promises.readdir(directory, { withFileTypes: true })) {\n const target = path.join(directory, entry.name);\n newest = Math.max(newest, (await fs.promises.stat(target)).mtimeMs);\n if (!entry.isDirectory()) continue;\n for (const child of await fs.promises.readdir(target)) {\n newest = Math.max(newest, (await fs.promises.stat(path.join(target, child))).mtimeMs);\n }\n }\n return newest;\n}\n\n/**\n * Whether any run in this session names a pi process that is still running.\n *\n * One live record is enough: records are only ever written by their owner, so a\n * live host pid means the session is still in use however old its files look.\n */\nfunction ownerAlive(stateDirectory: string): boolean {\n let entries: string[];\n try {\n entries = fs.readdirSync(stateDirectory);\n } catch {\n // No records left to consult, so nothing claims the directory.\n return false;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(STATE_EXTENSION)) continue;\n try {\n const value: unknown = JSON.parse(fs.readFileSync(path.join(stateDirectory, entry), FILE_ENCODING));\n const hostPid = (value as { hostPid?: unknown }).hostPid;\n if (typeof hostPid !== 'number') continue;\n process.kill(hostPid, 0);\n return true;\n } catch {\n // An unreadable record and a dead pid mean the same thing here: no claim.\n }\n }\n return false;\n}\n\nasync function ownerAliveAsync(stateDirectory: string): Promise<boolean> {\n let entries: string[];\n try {\n entries = await fs.promises.readdir(stateDirectory);\n } catch {\n return false;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(STATE_EXTENSION)) continue;\n try {\n const value: unknown = JSON.parse(await fs.promises.readFile(path.join(stateDirectory, entry), FILE_ENCODING));\n const hostPid = (value as { hostPid?: unknown }).hostPid;\n if (typeof hostPid !== 'number') continue;\n process.kill(hostPid, 0);\n return true;\n } catch {\n // An unreadable record and a dead pid mean the same thing here: no claim.\n }\n }\n return false;\n}\n\nfunction completedAt(value: unknown): number | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const record = value as { state?: unknown; exit?: { finishedAt?: unknown } };\n if (record.state !== 'completed' || typeof record.exit?.finishedAt !== 'string') return undefined;\n const timestamp = Date.parse(record.exit.finishedAt);\n return Number.isFinite(timestamp) ? timestamp : undefined;\n}\n\n/** Session-scoped runner storage under the Pi agent directory. */\nexport class RunnerPaths implements IRunnerPaths {\n private adoptedSessionId: string | undefined;\n private repository: string | undefined;\n\n repositoryPath(): string {\n this.repository ??= realPath(git(['rev-parse', '--show-toplevel'], process.cwd()) ?? process.cwd());\n return this.repository;\n }\n\n setSessionId(sessionId: string): void {\n const trimmed = sessionId.trim();\n if (!trimmed) throw new Error('doom-runner session id cannot be blank');\n this.adoptedSessionId = trimmed;\n }\n\n logDirectory(sessionId?: string): string {\n const override = process.env[LOG_DIR_ENV]?.trim();\n if (override) return path.resolve(override);\n return path.join(this.sessionDirectory(sessionId), LOG_DIR_NAME);\n }\n\n stateDirectory(sessionId?: string): string {\n const override = process.env[LOG_DIR_ENV]?.trim();\n if (override) return path.join(path.resolve(override), '..', STATE_DIR_NAME);\n return path.join(this.sessionDirectory(sessionId), STATE_DIR_NAME);\n }\n\n logPathFor(id: string, sessionId?: string): string {\n return path.join(this.logDirectory(sessionId), `${id}${LOG_EXTENSION}`);\n }\n\n rotatedLogPathFor(id: string, sessionId?: string): string {\n return `${this.logPathFor(id, sessionId)}${ROTATED_SUFFIX}`;\n }\n\n statePathFor(id: string, sessionId?: string): string {\n return path.join(this.stateDirectory(sessionId), `${id}${STATE_EXTENSION}`);\n }\n\n ensureDirectories(sessionId?: string): void {\n fs.mkdirSync(this.logDirectory(sessionId), { recursive: true });\n fs.mkdirSync(this.stateDirectory(sessionId), { recursive: true });\n }\n\n sweepHistory(ttlMs: number, now: number = Date.now()): LogSweepResult {\n const result: LogSweepResult = { removed: [], errors: [] };\n // A pinned log directory belongs to whoever pinned it, so that branch only\n // ever removes the files this package put there.\n if (process.env[LOG_DIR_ENV]?.trim()) {\n this.sweepStateDirectory(this.stateDirectory(), this.sessionId(), ttlMs, now, result);\n return result;\n }\n const root = this.storeDirectory();\n let sessions: string[];\n try {\n sessions = fs.readdirSync(root);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return result;\n result.errors.push(`Could not read runner history directory ${root}: ${String(error)}`);\n return result;\n }\n\n // Sweeping whole session directories rather than the three files a completed\n // record names is what keeps sockets, sidecars, runs that never reached a\n // terminal state, and the emptied directories themselves from accumulating.\n const active = this.currentSessionId();\n for (const sessionId of sessions) {\n if (sessionId === active) continue;\n this.sweepSessionDirectory(path.join(root, sessionId), ttlMs, now, result);\n }\n return result;\n }\n\n async sweepHistoryAsync(ttlMs: number, now: number = Date.now()): Promise<LogSweepResult> {\n const result: LogSweepResult = { removed: [], errors: [] };\n if (process.env[LOG_DIR_ENV]?.trim()) {\n await this.sweepStateDirectoryAsync(this.stateDirectory(), this.sessionId(), ttlMs, now, result);\n return result;\n }\n\n const root = this.storeDirectory();\n let sessions: string[];\n try {\n sessions = await fs.promises.readdir(root);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return result;\n result.errors.push(`Could not read runner history directory ${root}: ${String(error)}`);\n return result;\n }\n\n const active = this.currentSessionId();\n for (const sessionId of sessions) {\n if (sessionId === active) continue;\n await this.sweepSessionDirectoryAsync(path.join(root, sessionId), ttlMs, now, result);\n }\n return result;\n }\n\n legacyDirectory(): string | undefined {\n const commonDirectory = git(['rev-parse', '--path-format=absolute', '--git-common-dir'], process.cwd());\n return commonDirectory ? path.join(commonDirectory, STORE_DIR_NAME) : undefined;\n }\n\n removeLegacyStore(): string | undefined {\n const target = this.legacyDirectory();\n if (!target || path.basename(target) !== STORE_DIR_NAME || !fs.existsSync(target)) return undefined;\n fs.rmSync(target, { recursive: true, force: true });\n return target;\n }\n\n /** The live session, if one is known, so a sweep never deletes underneath itself. */\n private currentSessionId(): string | undefined {\n try {\n return this.sessionId();\n } catch {\n // A CLI invocation outside a session still sweeps; it just has nothing to spare.\n return undefined;\n }\n }\n\n /** Removes a session's whole directory once its newest entry is past the TTL. */\n private sweepSessionDirectory(directory: string, ttlMs: number, now: number, result: LogSweepResult): void {\n try {\n if (now - newestEntryTime(directory) <= ttlMs) return;\n // An idle session can go quiet for longer than the TTL, and deleting one\n // takes its lifeline socket with it, which would read to its own runners\n // as an owner that had died. Its records name the owner, so ask.\n if (ownerAlive(path.join(directory, STATE_DIR_NAME))) return;\n fs.rmSync(directory, { recursive: true, force: true });\n result.removed.push(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not sweep runner session ${directory}: ${String(error)}`);\n }\n }\n }\n\n private async sweepSessionDirectoryAsync(\n directory: string,\n ttlMs: number,\n now: number,\n result: LogSweepResult,\n ): Promise<void> {\n try {\n if (now - (await newestEntryTimeAsync(directory)) <= ttlMs) return;\n if (await ownerAliveAsync(path.join(directory, STATE_DIR_NAME))) return;\n await fs.promises.rm(directory, { recursive: true, force: true });\n result.removed.push(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not sweep runner session ${directory}: ${String(error)}`);\n }\n }\n }\n\n private sessionId(explicit?: string): string {\n const sessionId = explicit?.trim() || this.adoptedSessionId || inheritedSessionId();\n if (!sessionId) throw new Error('PI_SESSION_ID is required for doom-runner storage');\n return sessionId;\n }\n\n private storeDirectory(): string {\n const override = process.env[LOG_DIR_ENV]?.trim();\n if (override) return path.dirname(path.resolve(override));\n return path.join(resolveAgentDirectory(process.env), STORE_DIR_NAME);\n }\n\n private sessionDirectory(sessionId?: string): string {\n return path.join(this.storeDirectory(), this.sessionId(sessionId));\n }\n\n private sweepStateDirectory(\n directory: string,\n sessionId: string,\n ttlMs: number,\n now: number,\n result: LogSweepResult,\n ): void {\n let entries: string[];\n try {\n entries = fs.readdirSync(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not read runner state directory ${directory}: ${String(error)}`);\n }\n return;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(STATE_EXTENSION)) continue;\n const statePath = path.join(directory, entry);\n try {\n const value: unknown = JSON.parse(fs.readFileSync(statePath, FILE_ENCODING));\n const finishedAt = completedAt(value);\n if (finishedAt === undefined || now - finishedAt <= ttlMs) continue;\n const id = entry.slice(0, -STATE_EXTENSION.length);\n this.removeHistoryFiles(sessionId, id, statePath, result);\n } catch (error) {\n result.errors.push(`Could not sweep runner metadata ${statePath}: ${String(error)}`);\n }\n }\n }\n\n private async sweepStateDirectoryAsync(\n directory: string,\n sessionId: string,\n ttlMs: number,\n now: number,\n result: LogSweepResult,\n ): Promise<void> {\n let entries: string[];\n try {\n entries = await fs.promises.readdir(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not read runner state directory ${directory}: ${String(error)}`);\n }\n return;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(STATE_EXTENSION)) continue;\n const statePath = path.join(directory, entry);\n try {\n const value: unknown = JSON.parse(await fs.promises.readFile(statePath, FILE_ENCODING));\n const finishedAt = completedAt(value);\n if (finishedAt === undefined || now - finishedAt <= ttlMs) continue;\n const id = entry.slice(0, -STATE_EXTENSION.length);\n await this.removeHistoryFilesAsync(sessionId, id, statePath, result);\n } catch (error) {\n result.errors.push(`Could not sweep runner metadata ${statePath}: ${String(error)}`);\n }\n }\n }\n\n private removeHistoryFiles(sessionId: string, id: string, statePath: string, result: LogSweepResult): void {\n const candidates = [statePath, this.logPathFor(id, sessionId), this.rotatedLogPathFor(id, sessionId)];\n for (const candidate of candidates) {\n try {\n fs.unlinkSync(candidate);\n result.removed.push(candidate);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not remove runner history ${candidate}: ${String(error)}`);\n }\n }\n }\n }\n\n private async removeHistoryFilesAsync(\n sessionId: string,\n id: string,\n statePath: string,\n result: LogSweepResult,\n ): Promise<void> {\n const candidates = [statePath, this.logPathFor(id, sessionId), this.rotatedLogPathFor(id, sessionId)];\n for (const candidate of candidates) {\n try {\n await fs.promises.unlink(candidate);\n result.removed.push(candidate);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not remove runner history ${candidate}: ${String(error)}`);\n }\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"RunnerPaths.mjs","names":[],"sources":["../../../src/adapters/RunnerPaths.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport type { IRunnerPaths, LogSweepResult } from '../services/RunnerPaths/types.ts';\nimport { inheritedSessionId } from '../services/runs/session.ts';\nimport { LOG_DIR_ENV } from '../types/config.ts';\n\nconst STORE_DIR_NAME = 'doom-runner';\nconst DEFAULT_CONFIG_DIR_NAME = '.pi';\nconst AGENT_DIR_NAME = 'agent';\nconst LOG_DIR_NAME = 'logs';\nconst STATE_DIR_NAME = 'runs';\nconst LOG_EXTENSION = '.log';\nconst STATE_EXTENSION = '.json';\nconst ROTATED_SUFFIX = '.1';\nconst PI_CODING_AGENT_DIR_ENV = 'PI_CODING_AGENT_DIR';\nconst HOME_ALIAS = '~';\nconst HOME_ALIAS_PREFIX = '~/';\nconst FILE_ENCODING = 'utf8';\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\n\nfunction git(args: string[], cwd: string): string | undefined {\n try {\n const output = execFileSync('git', args, { cwd, encoding: FILE_ENCODING, stdio: ['ignore', 'pipe', 'ignore'] });\n const trimmed = output.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction realPath(target: string): string {\n try {\n return fs.realpathSync(target);\n } catch {\n return target;\n }\n}\n\nfunction resolveAgentDirectory(env: NodeJS.ProcessEnv): string {\n const configured = env[PI_CODING_AGENT_DIR_ENV]?.trim();\n if (configured === HOME_ALIAS) return os.homedir();\n if (configured?.startsWith(HOME_ALIAS_PREFIX)) {\n return path.join(os.homedir(), configured.slice(HOME_ALIAS_PREFIX.length));\n }\n return configured ? path.resolve(configured) : path.join(os.homedir(), DEFAULT_CONFIG_DIR_NAME, AGENT_DIR_NAME);\n}\n\n/**\n * Newest mtime anywhere one level inside the session, falling back to the\n * directory itself. A session still being written to therefore always looks\n * recent, whatever state its individual records are in.\n */\nfunction newestEntryTime(directory: string): number {\n let newest = fs.statSync(directory).mtimeMs;\n for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {\n const target = path.join(directory, entry.name);\n newest = Math.max(newest, fs.statSync(target).mtimeMs);\n if (!entry.isDirectory()) continue;\n for (const child of fs.readdirSync(target)) {\n newest = Math.max(newest, fs.statSync(path.join(target, child)).mtimeMs);\n }\n }\n return newest;\n}\n\nasync function newestEntryTimeAsync(directory: string): Promise<number> {\n let newest = (await fs.promises.stat(directory)).mtimeMs;\n for (const entry of await fs.promises.readdir(directory, { withFileTypes: true })) {\n const target = path.join(directory, entry.name);\n newest = Math.max(newest, (await fs.promises.stat(target)).mtimeMs);\n if (!entry.isDirectory()) continue;\n for (const child of await fs.promises.readdir(target)) {\n newest = Math.max(newest, (await fs.promises.stat(path.join(target, child))).mtimeMs);\n }\n }\n return newest;\n}\n\n/**\n * Whether any run in this session names a pi process that is still running.\n *\n * One live record is enough: records are only ever written by their owner, so a\n * live host pid means the session is still in use however old its files look.\n */\nfunction ownerAlive(stateDirectory: string): boolean {\n let entries: string[];\n try {\n entries = fs.readdirSync(stateDirectory);\n } catch {\n // No records left to consult, so nothing claims the directory.\n return false;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(STATE_EXTENSION)) continue;\n try {\n const value: unknown = JSON.parse(fs.readFileSync(path.join(stateDirectory, entry), FILE_ENCODING));\n const hostPid = (value as { hostPid?: unknown }).hostPid;\n if (typeof hostPid !== 'number') continue;\n process.kill(hostPid, 0);\n return true;\n } catch {\n // An unreadable record and a dead pid mean the same thing here: no claim.\n }\n }\n return false;\n}\n\nasync function ownerAliveAsync(stateDirectory: string): Promise<boolean> {\n let entries: string[];\n try {\n entries = await fs.promises.readdir(stateDirectory);\n } catch {\n return false;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(STATE_EXTENSION)) continue;\n try {\n const value: unknown = JSON.parse(await fs.promises.readFile(path.join(stateDirectory, entry), FILE_ENCODING));\n const hostPid = (value as { hostPid?: unknown }).hostPid;\n if (typeof hostPid !== 'number') continue;\n process.kill(hostPid, 0);\n return true;\n } catch {\n // An unreadable record and a dead pid mean the same thing here: no claim.\n }\n }\n return false;\n}\n\nfunction completedAt(value: unknown): number | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const record = value as { state?: unknown; exit?: { finishedAt?: unknown } };\n if (record.state !== 'completed' || typeof record.exit?.finishedAt !== 'string') return undefined;\n const timestamp = Date.parse(record.exit.finishedAt);\n return Number.isFinite(timestamp) ? timestamp : undefined;\n}\n\n/**\n * The root every session's runner state lives under, before any log-directory\n * override: `<agent dir>/doom-runner/<session id>/{runs,logs}`. A reader in\n * another process, such as the cockpit hub, resolves the same root from its\n * own environment.\n */\nexport function resolveRunnerStoreDirectory(env: NodeJS.ProcessEnv): string {\n return path.join(resolveAgentDirectory(env), STORE_DIR_NAME);\n}\n\n/** Session-scoped runner storage under the Pi agent directory. */\nexport class RunnerPaths implements IRunnerPaths {\n private adoptedSessionId: string | undefined;\n private repository: string | undefined;\n\n repositoryPath(): string {\n this.repository ??= realPath(git(['rev-parse', '--show-toplevel'], process.cwd()) ?? process.cwd());\n return this.repository;\n }\n\n setSessionId(sessionId: string): void {\n const trimmed = sessionId.trim();\n if (!trimmed) throw new Error('doom-runner session id cannot be blank');\n this.adoptedSessionId = trimmed;\n }\n\n logDirectory(sessionId?: string): string {\n const override = process.env[LOG_DIR_ENV]?.trim();\n if (override) return path.resolve(override);\n return path.join(this.sessionDirectory(sessionId), LOG_DIR_NAME);\n }\n\n stateDirectory(sessionId?: string): string {\n const override = process.env[LOG_DIR_ENV]?.trim();\n if (override) return path.join(path.resolve(override), '..', STATE_DIR_NAME);\n return path.join(this.sessionDirectory(sessionId), STATE_DIR_NAME);\n }\n\n logPathFor(id: string, sessionId?: string): string {\n return path.join(this.logDirectory(sessionId), `${id}${LOG_EXTENSION}`);\n }\n\n rotatedLogPathFor(id: string, sessionId?: string): string {\n return `${this.logPathFor(id, sessionId)}${ROTATED_SUFFIX}`;\n }\n\n statePathFor(id: string, sessionId?: string): string {\n return path.join(this.stateDirectory(sessionId), `${id}${STATE_EXTENSION}`);\n }\n\n ensureDirectories(sessionId?: string): void {\n fs.mkdirSync(this.logDirectory(sessionId), { recursive: true });\n fs.mkdirSync(this.stateDirectory(sessionId), { recursive: true });\n }\n\n sweepHistory(ttlMs: number, now: number = Date.now()): LogSweepResult {\n const result: LogSweepResult = { removed: [], errors: [] };\n // A pinned log directory belongs to whoever pinned it, so that branch only\n // ever removes the files this package put there.\n if (process.env[LOG_DIR_ENV]?.trim()) {\n this.sweepStateDirectory(this.stateDirectory(), this.sessionId(), ttlMs, now, result);\n return result;\n }\n const root = this.storeDirectory();\n let sessions: string[];\n try {\n sessions = fs.readdirSync(root);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return result;\n result.errors.push(`Could not read runner history directory ${root}: ${String(error)}`);\n return result;\n }\n\n // Sweeping whole session directories rather than the three files a completed\n // record names is what keeps sockets, sidecars, runs that never reached a\n // terminal state, and the emptied directories themselves from accumulating.\n const active = this.currentSessionId();\n for (const sessionId of sessions) {\n if (sessionId === active) continue;\n this.sweepSessionDirectory(path.join(root, sessionId), ttlMs, now, result);\n }\n return result;\n }\n\n async sweepHistoryAsync(ttlMs: number, now: number = Date.now()): Promise<LogSweepResult> {\n const result: LogSweepResult = { removed: [], errors: [] };\n if (process.env[LOG_DIR_ENV]?.trim()) {\n await this.sweepStateDirectoryAsync(this.stateDirectory(), this.sessionId(), ttlMs, now, result);\n return result;\n }\n\n const root = this.storeDirectory();\n let sessions: string[];\n try {\n sessions = await fs.promises.readdir(root);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return result;\n result.errors.push(`Could not read runner history directory ${root}: ${String(error)}`);\n return result;\n }\n\n const active = this.currentSessionId();\n for (const sessionId of sessions) {\n if (sessionId === active) continue;\n await this.sweepSessionDirectoryAsync(path.join(root, sessionId), ttlMs, now, result);\n }\n return result;\n }\n\n legacyDirectory(): string | undefined {\n const commonDirectory = git(['rev-parse', '--path-format=absolute', '--git-common-dir'], process.cwd());\n return commonDirectory ? path.join(commonDirectory, STORE_DIR_NAME) : undefined;\n }\n\n removeLegacyStore(): string | undefined {\n const target = this.legacyDirectory();\n if (!target || path.basename(target) !== STORE_DIR_NAME || !fs.existsSync(target)) return undefined;\n fs.rmSync(target, { recursive: true, force: true });\n return target;\n }\n\n /** The live session, if one is known, so a sweep never deletes underneath itself. */\n private currentSessionId(): string | undefined {\n try {\n return this.sessionId();\n } catch {\n // A CLI invocation outside a session still sweeps; it just has nothing to spare.\n return undefined;\n }\n }\n\n /** Removes a session's whole directory once its newest entry is past the TTL. */\n private sweepSessionDirectory(directory: string, ttlMs: number, now: number, result: LogSweepResult): void {\n try {\n if (now - newestEntryTime(directory) <= ttlMs) return;\n // An idle session can go quiet for longer than the TTL, and deleting one\n // takes its lifeline socket with it, which would read to its own runners\n // as an owner that had died. Its records name the owner, so ask.\n if (ownerAlive(path.join(directory, STATE_DIR_NAME))) return;\n fs.rmSync(directory, { recursive: true, force: true });\n result.removed.push(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not sweep runner session ${directory}: ${String(error)}`);\n }\n }\n }\n\n private async sweepSessionDirectoryAsync(\n directory: string,\n ttlMs: number,\n now: number,\n result: LogSweepResult,\n ): Promise<void> {\n try {\n if (now - (await newestEntryTimeAsync(directory)) <= ttlMs) return;\n if (await ownerAliveAsync(path.join(directory, STATE_DIR_NAME))) return;\n await fs.promises.rm(directory, { recursive: true, force: true });\n result.removed.push(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not sweep runner session ${directory}: ${String(error)}`);\n }\n }\n }\n\n private sessionId(explicit?: string): string {\n const sessionId = explicit?.trim() || this.adoptedSessionId || inheritedSessionId();\n if (!sessionId) throw new Error('PI_SESSION_ID is required for doom-runner storage');\n return sessionId;\n }\n\n private storeDirectory(): string {\n const override = process.env[LOG_DIR_ENV]?.trim();\n if (override) return path.dirname(path.resolve(override));\n return resolveRunnerStoreDirectory(process.env);\n }\n\n private sessionDirectory(sessionId?: string): string {\n return path.join(this.storeDirectory(), this.sessionId(sessionId));\n }\n\n private sweepStateDirectory(\n directory: string,\n sessionId: string,\n ttlMs: number,\n now: number,\n result: LogSweepResult,\n ): void {\n let entries: string[];\n try {\n entries = fs.readdirSync(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not read runner state directory ${directory}: ${String(error)}`);\n }\n return;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(STATE_EXTENSION)) continue;\n const statePath = path.join(directory, entry);\n try {\n const value: unknown = JSON.parse(fs.readFileSync(statePath, FILE_ENCODING));\n const finishedAt = completedAt(value);\n if (finishedAt === undefined || now - finishedAt <= ttlMs) continue;\n const id = entry.slice(0, -STATE_EXTENSION.length);\n this.removeHistoryFiles(sessionId, id, statePath, result);\n } catch (error) {\n result.errors.push(`Could not sweep runner metadata ${statePath}: ${String(error)}`);\n }\n }\n }\n\n private async sweepStateDirectoryAsync(\n directory: string,\n sessionId: string,\n ttlMs: number,\n now: number,\n result: LogSweepResult,\n ): Promise<void> {\n let entries: string[];\n try {\n entries = await fs.promises.readdir(directory);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not read runner state directory ${directory}: ${String(error)}`);\n }\n return;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(STATE_EXTENSION)) continue;\n const statePath = path.join(directory, entry);\n try {\n const value: unknown = JSON.parse(await fs.promises.readFile(statePath, FILE_ENCODING));\n const finishedAt = completedAt(value);\n if (finishedAt === undefined || now - finishedAt <= ttlMs) continue;\n const id = entry.slice(0, -STATE_EXTENSION.length);\n await this.removeHistoryFilesAsync(sessionId, id, statePath, result);\n } catch (error) {\n result.errors.push(`Could not sweep runner metadata ${statePath}: ${String(error)}`);\n }\n }\n }\n\n private removeHistoryFiles(sessionId: string, id: string, statePath: string, result: LogSweepResult): void {\n const candidates = [statePath, this.logPathFor(id, sessionId), this.rotatedLogPathFor(id, sessionId)];\n for (const candidate of candidates) {\n try {\n fs.unlinkSync(candidate);\n result.removed.push(candidate);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not remove runner history ${candidate}: ${String(error)}`);\n }\n }\n }\n }\n\n private async removeHistoryFilesAsync(\n sessionId: string,\n id: string,\n statePath: string,\n result: LogSweepResult,\n ): Promise<void> {\n const candidates = [statePath, this.logPathFor(id, sessionId), this.rotatedLogPathFor(id, sessionId)];\n for (const candidate of candidates) {\n try {\n await fs.promises.unlink(candidate);\n result.removed.push(candidate);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n result.errors.push(`Could not remove runner history ${candidate}: ${String(error)}`);\n }\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi-extension-contracts/child-process"),i=require("@agimon-ai/foundation-process-registry");const a=`tool`,o=`doom-runner:`,s=`doom-runner`;function c(){return(0,i.createProcessRegistryService)(process.env.PROCESS_REGISTRY_PATH)}function l(e){return`session:${e}`}function u(e,t){let n=e.metadata?.[t];return typeof n==`string`?n:``}function d(e,t){let n=e.metadata?.[t];return typeof n==`number`?n:0}var f=class{paths;processControl;registry;listeners=new Set;constructor(e,t,n){this.paths=e,this.processControl=t,this.registry=n}async register(e){let t=new Date().toISOString(),n=(0,r.resolveRootSessionId)(e.sessionId),c=await this.registry.registerProcess({repositoryPath:this.repositoryPath(),serviceName:`${o}${e.id}`,serviceType:a,pid:e.pid,command:e.command,tags:(0,i.resolveProcessTags)([s,`runner:${e.id}`,l(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:n,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},startedAt:t,hostPid:process.pid},force:!0});if(!c.success)throw Error(c.error??`Failed to register runner ${e.name}`);let u={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:n,startedAt:t,state:`running`,promoted:!1,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},...e.alarmMs?{alarm:{intervalMs:e.alarmMs,lastFiredAt:t}}:{},hostPid:process.pid};return this.writeRecord(u),this.notify(),u}async list(){return(await this.registry.listProcesses({repositoryPath:this.repositoryPath(),serviceType:a,tags:(0,i.resolveProcessTags)([s])})).map(p)}async listAcrossRepositories(){return(await this.registry.listProcesses({serviceType:a,tags:(0,i.resolveProcessTags)([s])})).map(p)}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 n=[],r=this.paths.stateDirectory(e);for(let e of t.default.readdirSync(r))if(h(e))try{let i=JSON.parse(t.default.readFileSync(`${r}/${e}`,`utf8`));g(i)&&n.push(i)}catch(t){process.emitWarning(`Could not read runner metadata ${e}: ${String(t)}`)}return n.sort((e,t)=>t.startedAt.localeCompare(e.startedAt))}async get(e,r){let i=this.paths.statePathFor(e,r);try{let r=JSON.parse(t.default.readFileSync(i,`utf8`));if(!g(r)||r.id!==e){process.emitWarning(`Could not read runner metadata ${n.default.basename(i)}: invalid runner record`);return}return r}catch(e){if(e.code===`ENOENT`)return;process.emitWarning(`Could not read runner metadata ${n.default.basename(i)}: ${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 clearAlarm(e,t){let n=await this.get(e,t);if(!n)return;if(!n.alarm)return n;let r={...n};return delete r.alarm,this.writeRecord(r),this.notify(),r}async markAlarmFired(e,t){let n=await this.get(e);if(!n?.alarm||n.state!==`running`)return;let r={...n,alarm:{...n.alarm,lastFiredAt:t}};return this.writeRecord(r),this.notify(),r}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:`${o}${e}`,serviceType:a,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(0,i.normalizeRepositoryPath)(this.paths.repositoryPath())}writeRecord(e){this.paths.ensureDirectories(e.sessionId);let n=this.paths.statePathFor(e.id,e.sessionId),r=`${n}.${process.pid}.tmp`;t.default.writeFileSync(r,`${JSON.stringify(e,null,2)}\n`,`utf8`),t.default.renameSync(r,n)}};function p(e){return{id:u(e,`id`),name:u(e,`name`),pid:e.pid,command:u(e,`command`),cwd:u(e,`cwd`),logPath:u(e,`logPath`),interactive:e.metadata?.interactive===!0,sessionId:u(e,`sessionId`),...u(e,`rootSessionId`)?{rootSessionId:u(e,`rootSessionId`)}:{},startedAt:u(e,`startedAt`),state:`running`,promoted:!1,backend:m(e.metadata?.backend),...u(e,`backendTarget`)?{backendTarget:u(e,`backendTarget`)}:{},hostPid:d(e,`hostPid`)}}function m(e){return e===`rmux`||e===`tmux`?e:`native`}function h(e){return e.endsWith(`.json`)&&!e.endsWith(`.command.json`)&&!e.endsWith(`.exit.json`)}function g(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.id==`string`&&typeof t.name==`string`&&typeof t.pid==`number`&&typeof t.command==`string`&&typeof t.cwd==`string`&&typeof t.logPath==`string`&&typeof t.sessionId==`string`&&typeof t.startedAt==`string`&&(t.state===`running`||t.state===`completed`)&&typeof t.promoted==`boolean`&&(t.backend===`rmux`||t.backend===`tmux`||t.backend===`native`)}exports.RunnerRegistry=f,exports.createDefaultProcessRegistry=c;
1
+ const e=require("../../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/runs/runnerRecord.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("@agimon-ai/doompi-extension-contracts/child-process"),a=require("@agimon-ai/foundation-process-registry");const o=`tool`,s=`doom-runner:`,c=`doom-runner`;function l(){return(0,a.createProcessRegistryService)(process.env.PROCESS_REGISTRY_PATH)}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}var p=class{paths;processControl;registry;listeners=new Set;constructor(e,t,n){this.paths=e,this.processControl=t,this.registry=n}async register(e){let t=new Date().toISOString(),n=(0,i.resolveRootSessionId)(e.sessionId),r=await this.registry.registerProcess({repositoryPath:this.repositoryPath(),serviceName:`${s}${e.id}`,serviceType:o,pid:e.pid,command:e.command,tags:(0,a.resolveProcessTags)([c,`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:n,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},startedAt:t,hostPid:process.pid},force:!0});if(!r.success)throw Error(r.error??`Failed to register runner ${e.name}`);let l={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:n,startedAt:t,state:`running`,promoted:!1,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},...e.alarmMs?{alarm:{intervalMs:e.alarmMs,lastFiredAt:t}}:{},hostPid:process.pid};return this.writeRecord(l),this.notify(),l}async list(){return(await this.registry.listProcesses({repositoryPath:this.repositoryPath(),serviceType:o,tags:(0,a.resolveProcessTags)([c])})).map(m)}async listAcrossRepositories(){return(await this.registry.listProcesses({serviceType:o,tags:(0,a.resolveProcessTags)([c])})).map(m)}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 r=[],i=this.paths.stateDirectory(e);for(let e of n.default.readdirSync(i))if(g(e))try{let a=JSON.parse(n.default.readFileSync(`${i}/${e}`,`utf8`));t.isRunnerRecord(a)&&r.push(a)}catch(t){process.emitWarning(`Could not read runner metadata ${e}: ${String(t)}`)}return r.sort((e,t)=>t.startedAt.localeCompare(e.startedAt))}async get(e,i){let a=this.paths.statePathFor(e,i);try{let i=JSON.parse(n.default.readFileSync(a,`utf8`));if(!t.isRunnerRecord(i)||i.id!==e){process.emitWarning(`Could not read runner metadata ${r.default.basename(a)}: invalid runner record`);return}return i}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 clearAlarm(e,t){let n=await this.get(e,t);if(!n)return;if(!n.alarm)return n;let r={...n};return delete r.alarm,this.writeRecord(r),this.notify(),r}async markAlarmFired(e,t){let n=await this.get(e);if(!n?.alarm||n.state!==`running`)return;let r={...n,alarm:{...n.alarm,lastFiredAt:t}};return this.writeRecord(r),this.notify(),r}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:`${s}${e}`,serviceType:o,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(0,a.normalizeRepositoryPath)(this.paths.repositoryPath())}writeRecord(e){this.paths.ensureDirectories(e.sessionId);let t=this.paths.statePathFor(e.id,e.sessionId),r=`${t}.${process.pid}.tmp`;n.default.writeFileSync(r,`${JSON.stringify(e,null,2)}\n`,`utf8`),n.default.renameSync(r,t)}};function m(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:h(e.metadata?.backend),...d(e,`backendTarget`)?{backendTarget:d(e,`backendTarget`)}:{},hostPid:f(e,`hostPid`)}}function h(e){return e===`rmux`||e===`tmux`?e:`native`}function g(e){return e.endsWith(`.json`)&&!e.endsWith(`.command.json`)&&!e.endsWith(`.exit.json`)}exports.RunnerRegistry=p,exports.createDefaultProcessRegistry=l;
2
2
  //# sourceMappingURL=RunnerRegistry.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"RunnerRegistry.cjs","names":["createProcessRegistryService","resolveRootSessionId","resolveProcessTags","fs","path","normalizeRepositoryPath"],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IRunnerPaths } from '../../services/RunnerPaths/types';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\n\nconst SERVICE_TYPE = 'tool';\nconst SERVICE_PREFIX = 'doom-runner:';\nconst RUNNER_TAG = 'doom-runner';\nconst REGISTRY_PATH_ENV = 'PROCESS_REGISTRY_PATH';\nconst JSON_EXTENSION = '.json';\nconst COMMAND_SIDECAR_SUFFIX = '.command.json';\nconst EXIT_SIDECAR_SUFFIX = '.exit.json';\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\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 ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId);\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 ...(input.alarmMs ? { alarm: { intervalMs: input.alarmMs, lastFiredAt: startedAt } } : {}),\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 clearAlarm(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id, sessionId);\n if (!record) return undefined;\n if (!record.alarm) return record;\n const cleared: RunnerRecord = { ...record };\n delete cleared.alarm;\n this.writeRecord(cleared);\n this.notify();\n return cleared;\n }\n\n async markAlarmFired(id: string, firedAt: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record?.alarm || record.state !== 'running') return undefined;\n const fired: RunnerRecord = { ...record, alarm: { ...record.alarm, lastFiredAt: firedAt } };\n this.writeRecord(fired);\n this.notify();\n return fired;\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\nfunction isRunnerRecord(value: unknown): value is RunnerRecord {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<RunnerRecord>;\n return (\n typeof record.id === 'string' &&\n typeof record.name === 'string' &&\n typeof record.pid === 'number' &&\n typeof record.command === 'string' &&\n typeof record.cwd === 'string' &&\n typeof record.logPath === 'string' &&\n typeof record.sessionId === 'string' &&\n typeof record.startedAt === 'string' &&\n (record.state === 'running' || record.state === 'completed') &&\n typeof record.promoted === 'boolean' &&\n (record.backend === 'rmux' || record.backend === 'tmux' || record.backend === 'native')\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"RunnerRegistry.cjs","names":["createProcessRegistryService","resolveRootSessionId","resolveProcessTags","fs","isRunnerRecord","path","normalizeRepositoryPath"],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IRunnerPaths } from '../../services/RunnerPaths/types';\nimport { isRunnerRecord } from '../../services/runs/runnerRecord.ts';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\n\nconst SERVICE_TYPE = 'tool';\nconst SERVICE_PREFIX = 'doom-runner:';\nconst RUNNER_TAG = 'doom-runner';\nconst REGISTRY_PATH_ENV = 'PROCESS_REGISTRY_PATH';\nconst JSON_EXTENSION = '.json';\nconst COMMAND_SIDECAR_SUFFIX = '.command.json';\nconst EXIT_SIDECAR_SUFFIX = '.exit.json';\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\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 ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId);\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 ...(input.alarmMs ? { alarm: { intervalMs: input.alarmMs, lastFiredAt: startedAt } } : {}),\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 clearAlarm(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id, sessionId);\n if (!record) return undefined;\n if (!record.alarm) return record;\n const cleared: RunnerRecord = { ...record };\n delete cleared.alarm;\n this.writeRecord(cleared);\n this.notify();\n return cleared;\n }\n\n async markAlarmFired(id: string, firedAt: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record?.alarm || record.state !== 'running') return undefined;\n const fired: RunnerRecord = { ...record, alarm: { ...record.alarm, lastFiredAt: firedAt } };\n this.writeRecord(fired);\n this.notify();\n return fired;\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":"RunnerRegistry.d.cts","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"mappings":";;;;;;KA6BY,sBAAsB,KAChC;;iBAKc,gCAAgC;cAkBnC,0BAA0B;mBAIlB;mBACA;mBACA;mBALF;EAEjB,YACmB,OAAO,cACP,gBAAgB,iBAChB,UAAU;EAGvB,SAAS,OAAO,sBAAsB,QAAQ;EAoD9C,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,WAAW,YAAY,qBAAqB,QAAQ;EAWpD,eAAe,YAAY,kBAAkB,QAAQ;EASrD,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;EA0BhF,QAAQ,aAAa;EAerB,aAAa;EAQnB,UAAU;EAKV;UAKQ;UAIA;UAIA"}
1
+ {"version":3,"file":"RunnerRegistry.d.cts","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"mappings":";;;;;;KA8BY,sBAAsB,KAChC;;iBAKc,gCAAgC;cAkBnC,0BAA0B;mBAIlB;mBACA;mBACA;mBALF;EAEjB,YACmB,OAAO,cACP,gBAAgB,iBAChB,UAAU;EAGvB,SAAS,OAAO,sBAAsB,QAAQ;EAoD9C,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,WAAW,YAAY,qBAAqB,QAAQ;EAWpD,eAAe,YAAY,kBAAkB,QAAQ;EASrD,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;EA0BhF,QAAQ,aAAa;EAerB,aAAa;EAQnB,UAAU;EAKV;UAKQ;UAIA;UAIA"}
@@ -1 +1 @@
1
- {"version":3,"file":"RunnerRegistry.d.mts","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"mappings":";;;;;;KA6BY,sBAAsB,KAChC;;iBAKc,gCAAgC;cAkBnC,0BAA0B;mBAIlB;mBACA;mBACA;mBALF;EAEjB,YACmB,OAAO,cACP,gBAAgB,iBAChB,UAAU;EAGvB,SAAS,OAAO,sBAAsB,QAAQ;EAoD9C,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,WAAW,YAAY,qBAAqB,QAAQ;EAWpD,eAAe,YAAY,kBAAkB,QAAQ;EASrD,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;EA0BhF,QAAQ,aAAa;EAerB,aAAa;EAQnB,UAAU;EAKV;UAKQ;UAIA;UAIA"}
1
+ {"version":3,"file":"RunnerRegistry.d.mts","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"mappings":";;;;;;KA8BY,sBAAsB,KAChC;;iBAKc,gCAAgC;cAkBnC,0BAA0B;mBAIlB;mBACA;mBACA;mBALF;EAEjB,YACmB,OAAO,cACP,gBAAgB,iBAChB,UAAU;EAGvB,SAAS,OAAO,sBAAsB,QAAQ;EAoD9C,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,WAAW,YAAY,qBAAqB,QAAQ;EAWpD,eAAe,YAAY,kBAAkB,QAAQ;EASrD,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;EA0BhF,QAAQ,aAAa;EAerB,aAAa;EAQnB,UAAU;EAKV;UAKQ;UAIA;UAIA"}
@@ -1,2 +1,2 @@
1
- import e from"node:fs";import t from"node:path";import{resolveRootSessionId as n}from"@agimon-ai/doompi-extension-contracts/child-process";import{createProcessRegistryService as r,normalizeRepositoryPath as i,resolveProcessTags as a}from"@agimon-ai/foundation-process-registry";const o=`tool`,s=`doom-runner:`,c=`doom-runner`;function l(){return r(process.env.PROCESS_REGISTRY_PATH)}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}var p=class{paths;processControl;registry;listeners=new Set;constructor(e,t,n){this.paths=e,this.processControl=t,this.registry=n}async register(e){let t=new Date().toISOString(),r=n(e.sessionId),i=await this.registry.registerProcess({repositoryPath:this.repositoryPath(),serviceName:`${s}${e.id}`,serviceType:o,pid:e.pid,command:e.command,tags:a([c,`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:t,hostPid:process.pid},force:!0});if(!i.success)throw Error(i.error??`Failed to register runner ${e.name}`);let l={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:t,state:`running`,promoted:!1,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},...e.alarmMs?{alarm:{intervalMs:e.alarmMs,lastFiredAt:t}}:{},hostPid:process.pid};return this.writeRecord(l),this.notify(),l}async list(){return(await this.registry.listProcesses({repositoryPath:this.repositoryPath(),serviceType:o,tags:a([c])})).map(m)}async listAcrossRepositories(){return(await this.registry.listProcesses({serviceType:o,tags:a([c])})).map(m)}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(t){this.paths.ensureDirectories(t);let n=[],r=this.paths.stateDirectory(t);for(let t of e.readdirSync(r))if(g(t))try{let i=JSON.parse(e.readFileSync(`${r}/${t}`,`utf8`));_(i)&&n.push(i)}catch(e){process.emitWarning(`Could not read runner metadata ${t}: ${String(e)}`)}return n.sort((e,t)=>t.startedAt.localeCompare(e.startedAt))}async get(n,r){let i=this.paths.statePathFor(n,r);try{let r=JSON.parse(e.readFileSync(i,`utf8`));if(!_(r)||r.id!==n){process.emitWarning(`Could not read runner metadata ${t.basename(i)}: invalid runner record`);return}return r}catch(e){if(e.code===`ENOENT`)return;process.emitWarning(`Could not read runner metadata ${t.basename(i)}: ${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 clearAlarm(e,t){let n=await this.get(e,t);if(!n)return;if(!n.alarm)return n;let r={...n};return delete r.alarm,this.writeRecord(r),this.notify(),r}async markAlarmFired(e,t){let n=await this.get(e);if(!n?.alarm||n.state!==`running`)return;let r={...n,alarm:{...n.alarm,lastFiredAt:t}};return this.writeRecord(r),this.notify(),r}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:`${s}${e}`,serviceType:o,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 i(this.paths.repositoryPath())}writeRecord(t){this.paths.ensureDirectories(t.sessionId);let n=this.paths.statePathFor(t.id,t.sessionId),r=`${n}.${process.pid}.tmp`;e.writeFileSync(r,`${JSON.stringify(t,null,2)}\n`,`utf8`),e.renameSync(r,n)}};function m(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:h(e.metadata?.backend),...d(e,`backendTarget`)?{backendTarget:d(e,`backendTarget`)}:{},hostPid:f(e,`hostPid`)}}function h(e){return e===`rmux`||e===`tmux`?e:`native`}function g(e){return e.endsWith(`.json`)&&!e.endsWith(`.command.json`)&&!e.endsWith(`.exit.json`)}function _(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.id==`string`&&typeof t.name==`string`&&typeof t.pid==`number`&&typeof t.command==`string`&&typeof t.cwd==`string`&&typeof t.logPath==`string`&&typeof t.sessionId==`string`&&typeof t.startedAt==`string`&&(t.state===`running`||t.state===`completed`)&&typeof t.promoted==`boolean`&&(t.backend===`rmux`||t.backend===`tmux`||t.backend===`native`)}export{p as RunnerRegistry,l as createDefaultProcessRegistry};
1
+ import{isRunnerRecord as e}from"../../services/runs/runnerRecord.mjs";import t from"node:fs";import n from"node:path";import{resolveRootSessionId as r}from"@agimon-ai/doompi-extension-contracts/child-process";import{createProcessRegistryService as i,normalizeRepositoryPath as a,resolveProcessTags as o}from"@agimon-ai/foundation-process-registry";const s=`tool`,c=`doom-runner:`,l=`doom-runner`;function u(){return i(process.env.PROCESS_REGISTRY_PATH)}function d(e){return`session:${e}`}function f(e,t){let n=e.metadata?.[t];return typeof n==`string`?n:``}function p(e,t){let n=e.metadata?.[t];return typeof n==`number`?n:0}var m=class{paths;processControl;registry;listeners=new Set;constructor(e,t,n){this.paths=e,this.processControl=t,this.registry=n}async register(e){let t=new Date().toISOString(),n=r(e.sessionId),i=await this.registry.registerProcess({repositoryPath:this.repositoryPath(),serviceName:`${c}${e.id}`,serviceType:s,pid:e.pid,command:e.command,tags:o([l,`runner:${e.id}`,d(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:n,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},startedAt:t,hostPid:process.pid},force:!0});if(!i.success)throw Error(i.error??`Failed to register runner ${e.name}`);let a={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:n,startedAt:t,state:`running`,promoted:!1,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},...e.alarmMs?{alarm:{intervalMs:e.alarmMs,lastFiredAt:t}}:{},hostPid:process.pid};return this.writeRecord(a),this.notify(),a}async list(){return(await this.registry.listProcesses({repositoryPath:this.repositoryPath(),serviceType:s,tags:o([l])})).map(h)}async listAcrossRepositories(){return(await this.registry.listProcesses({serviceType:s,tags:o([l])})).map(h)}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(n){this.paths.ensureDirectories(n);let r=[],i=this.paths.stateDirectory(n);for(let n of t.readdirSync(i))if(_(n))try{let a=JSON.parse(t.readFileSync(`${i}/${n}`,`utf8`));e(a)&&r.push(a)}catch(e){process.emitWarning(`Could not read runner metadata ${n}: ${String(e)}`)}return r.sort((e,t)=>t.startedAt.localeCompare(e.startedAt))}async get(r,i){let a=this.paths.statePathFor(r,i);try{let i=JSON.parse(t.readFileSync(a,`utf8`));if(!e(i)||i.id!==r){process.emitWarning(`Could not read runner metadata ${n.basename(a)}: invalid runner record`);return}return i}catch(e){if(e.code===`ENOENT`)return;process.emitWarning(`Could not read runner metadata ${n.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 clearAlarm(e,t){let n=await this.get(e,t);if(!n)return;if(!n.alarm)return n;let r={...n};return delete r.alarm,this.writeRecord(r),this.notify(),r}async markAlarmFired(e,t){let n=await this.get(e);if(!n?.alarm||n.state!==`running`)return;let r={...n,alarm:{...n.alarm,lastFiredAt:t}};return this.writeRecord(r),this.notify(),r}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:`${c}${e}`,serviceType:s,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 a(this.paths.repositoryPath())}writeRecord(e){this.paths.ensureDirectories(e.sessionId);let n=this.paths.statePathFor(e.id,e.sessionId),r=`${n}.${process.pid}.tmp`;t.writeFileSync(r,`${JSON.stringify(e,null,2)}\n`,`utf8`),t.renameSync(r,n)}};function h(e){return{id:f(e,`id`),name:f(e,`name`),pid:e.pid,command:f(e,`command`),cwd:f(e,`cwd`),logPath:f(e,`logPath`),interactive:e.metadata?.interactive===!0,sessionId:f(e,`sessionId`),...f(e,`rootSessionId`)?{rootSessionId:f(e,`rootSessionId`)}:{},startedAt:f(e,`startedAt`),state:`running`,promoted:!1,backend:g(e.metadata?.backend),...f(e,`backendTarget`)?{backendTarget:f(e,`backendTarget`)}:{},hostPid:p(e,`hostPid`)}}function g(e){return e===`rmux`||e===`tmux`?e:`native`}function _(e){return e.endsWith(`.json`)&&!e.endsWith(`.command.json`)&&!e.endsWith(`.exit.json`)}export{m as RunnerRegistry,u as createDefaultProcessRegistry};
2
2
  //# sourceMappingURL=RunnerRegistry.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"RunnerRegistry.mjs","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IRunnerPaths } from '../../services/RunnerPaths/types';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\n\nconst SERVICE_TYPE = 'tool';\nconst SERVICE_PREFIX = 'doom-runner:';\nconst RUNNER_TAG = 'doom-runner';\nconst REGISTRY_PATH_ENV = 'PROCESS_REGISTRY_PATH';\nconst JSON_EXTENSION = '.json';\nconst COMMAND_SIDECAR_SUFFIX = '.command.json';\nconst EXIT_SIDECAR_SUFFIX = '.exit.json';\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\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 ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId);\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 ...(input.alarmMs ? { alarm: { intervalMs: input.alarmMs, lastFiredAt: startedAt } } : {}),\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 clearAlarm(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id, sessionId);\n if (!record) return undefined;\n if (!record.alarm) return record;\n const cleared: RunnerRecord = { ...record };\n delete cleared.alarm;\n this.writeRecord(cleared);\n this.notify();\n return cleared;\n }\n\n async markAlarmFired(id: string, firedAt: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record?.alarm || record.state !== 'running') return undefined;\n const fired: RunnerRecord = { ...record, alarm: { ...record.alarm, lastFiredAt: firedAt } };\n this.writeRecord(fired);\n this.notify();\n return fired;\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\nfunction isRunnerRecord(value: unknown): value is RunnerRecord {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<RunnerRecord>;\n return (\n typeof record.id === 'string' &&\n typeof record.name === 'string' &&\n typeof record.pid === 'number' &&\n typeof record.command === 'string' &&\n typeof record.cwd === 'string' &&\n typeof record.logPath === 'string' &&\n typeof record.sessionId === 'string' &&\n typeof record.startedAt === 'string' &&\n (record.state === 'running' || record.state === 'completed') &&\n typeof record.promoted === 'boolean' &&\n (record.backend === 'rmux' || record.backend === 'tmux' || record.backend === 'native')\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"RunnerRegistry.mjs","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IRunnerPaths } from '../../services/RunnerPaths/types';\nimport { isRunnerRecord } from '../../services/runs/runnerRecord.ts';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\n\nconst SERVICE_TYPE = 'tool';\nconst SERVICE_PREFIX = 'doom-runner:';\nconst RUNNER_TAG = 'doom-runner';\nconst REGISTRY_PATH_ENV = 'PROCESS_REGISTRY_PATH';\nconst JSON_EXTENSION = '.json';\nconst COMMAND_SIDECAR_SUFFIX = '.command.json';\nconst EXIT_SIDECAR_SUFFIX = '.exit.json';\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\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 ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId);\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 ...(input.alarmMs ? { alarm: { intervalMs: input.alarmMs, lastFiredAt: startedAt } } : {}),\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 clearAlarm(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id, sessionId);\n if (!record) return undefined;\n if (!record.alarm) return record;\n const cleared: RunnerRecord = { ...record };\n delete cleared.alarm;\n this.writeRecord(cleared);\n this.notify();\n return cleared;\n }\n\n async markAlarmFired(id: string, firedAt: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record?.alarm || record.state !== 'running') return undefined;\n const fired: RunnerRecord = { ...record, alarm: { ...record.alarm, lastFiredAt: firedAt } };\n this.writeRecord(fired);\n this.notify();\n return fired;\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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"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../../_virtual/_rolldown/runtime.cjs"),t=require("../../types/config.cjs"),n=require("../../schemas/runnerSpec.cjs"),r=require("./TmuxClient.cjs");let i=require("node:crypto"),a=require("node:fs");a=e.__toESM(a,1);let o=require("node:path");o=e.__toESM(o,1);const s=/^doom-tmux-[A-Za-z0-9_-]+$/,c={NO_COLOR:`1`,CI:`1`};var l=class{paths;createClient;clientPromise;runs=new Map;completions=new Map;constructor(e,t=e=>new r.TmuxClient(e)){this.paths=e,this.createClient=t}async launch(e){let r=await this.client();if(!r)return;this.paths.ensureDirectories();let i=`doom-tmux-${e.id}`,o=this.paths.logPathFor(e.id),s=this.auxPaths(e.id,e.sessionId),c=!1,l=!1;try{this.prepareFiles(e,o,s);let u=n.shellJoin([process.execPath,n.runtimeEntry(`runnerHost`),s.spec,s.gate,s.exit]);await this.checked(r,[`new-session`,`-d`,`-s`,i,`-c`,e.cwd,u]),c=!0,await this.checked(r,[`set-option`,`-t`,i,`remain-on-exit`,`on`]),await this.checked(r,[`pipe-pane`,`-t`,i,this.logPipeCommand(o,s.logDone)]);let d=await p(r,i);a.default.writeFileSync(s.gate,``,{mode:384}),l=!0;let f=!0,m=this.monitor(r,i,s);this.trackCompletion(i,m);let h={id:e.id,name:e.name,pid:d,logPath:o,backend:`tmux`,backendTarget:i,output:()=>f?y(o,t.getResultMaxBytes()*2):``,completion:()=>m,detach:()=>{f=!1},stop:()=>this.stop(i,d)};return e.interactive&&this.runs.set(e.name,this.interactiveRun(h,r,i,m)),m.finally(()=>this.runs.delete(e.name)).catch(e=>process.emitWarning(`tmux completion failed for ${i}: ${x(e)}`)),h}catch(t){if(l)throw t;c&&await f(r,i),n.cleanupSupervisorFiles(s),process.emitWarning(e.interactive?`tmux launch unavailable for interactive runner: ${x(t)}`:`tmux launch unavailable, using supervised subprocess: ${x(t)}`);return}}async stop(e,t){if(!s.test(e)||!Number.isInteger(t)||t<=0)return!1;let n=await this.client();if(!n)return!1;try{if(await n.sessionMissing(e))return!0;let r=await p(n,e);if(r!==t)return!1;let i=v(r,e);return i===`gone`?!0:i===`failed`?!1:await g(n,e,3e3)||await n.sessionMissing(e)?!0:await p(n,e).catch(()=>void 0)===t&&f(n,e)}catch(t){return process.emitWarning(`Could not stop tmux target ${e}: ${x(t)}`),!1}}async watch(e,t,n){let r=await this.client();if(!r||await r.sessionMissing(t))return;let i=this.monitor(r,t,this.auxPaths(e,n));return this.trackCompletion(t,i),i}async input(e,t){let n=await this.client();return n?(await n.run([`send-keys`,`-t`,e,`-l`,t]).catch(t=>{process.emitWarning(`Could not send input to tmux target ${e}: ${x(t)}`)}))?.returnCode===0:!1}get(e){return this.runs.get(e)}readOutcome(e,t){return n.readExitMetadata(this.auxPaths(e,t).exit)}trackCompletion(e,t){this.completions.set(e,t),t.finally(()=>this.completions.delete(e)).catch(t=>process.emitWarning(`tmux monitor failed for ${e}: ${x(t)}`))}client(){return this.clientPromise??=this.resolveClient(),this.clientPromise}async resolveClient(){let e=this.createClient(u(this.paths.repositoryPath()));try{let t=await e.run([`-V`]);if(t.returnCode!==0)throw Error(t.stderr.trim()||`tmux reported no version`);return e}catch(e){process.emitWarning(`tmux is unavailable: ${x(e)}`);return}}async checked(e,t){let n=await e.run(t);if(n.returnCode!==0)throw Error(`tmux ${t[0]??``} failed: ${n.stderr.trim()||`exit ${n.returnCode}`}`)}prepareFiles(e,t,r){n.cleanupSupervisorFiles(r),a.default.writeFileSync(t,``,{mode:384});let i=d(e.sessionId,e.interactive),o={command:e.command,cwd:e.cwd,env:i};n.writeCommandSpec(r.spec,o)}auxPaths(e,t){return n.supervisorPaths(this.paths.stateDirectory(t),e)}logPipeCommand(e,r){return n.shellJoin([process.execPath,n.runtimeEntry(`logSink`),e,this.paths.rotatedLogPathFor(o.default.basename(e,`.log`)),String(t.getLogMaxBytes()),r])}async monitor(e,t,r){try{let i=await m(e,t,r.exit);return await h(e,t,r.logDone),n.readExitMetadata(r.exit)??{code:i,signal:null}}finally{await e.sessionMissing(t).catch(()=>!1)||await f(e,t),n.cleanupSupervisorFiles(r)}}interactiveRun(e,t,n,r){let i=``,a=!0,o=new Set;return(async()=>{for(;a;){let r=await t.run([`capture-pane`,`-p`,`-t`,n]).catch(t=>{process.emitWarning(`Could not capture tmux screen for ${e.name}: ${x(t)}`)}),a=r?.returnCode===0?r.stdout:i;if(a!==i){i=a;for(let e of o)e(a)}await b(100)}})(),r.finally(()=>{a=!1}).catch(t=>process.emitWarning(`tmux interactive completion failed for ${e.name}: ${x(t)}`)),{...e,write:e=>{t.run([`send-keys`,`-t`,n,`-l`,e]).catch(e=>process.emitWarning(x(e)))},screen:()=>i,onData:e=>(o.add(e),()=>o.delete(e)),resize:(e,r)=>{t.run([`resize-window`,`-t`,n,`-x`,String(e),`-y`,String(r)]).catch(e=>process.emitWarning(x(e)))}}}};function u(e){return`doom-tmux-${(0,i.createHash)(`sha256`).update(e).digest(`hex`).slice(0,12)}`}function d(e,t){let r={...process.env,...t?{}:c,...n.NO_TERMINAL_INPUT_ENV,PI_SESSION_ID:e};return t||delete r.FORCE_COLOR,Object.fromEntries(Object.entries(r).filter(e=>e[1]!==void 0))}async function f(e,t){return(await e.run([`kill-session`,`-t`,t]).catch(e=>{process.emitWarning(`Could not close tmux target ${t}: ${x(e)}`)}))?.returnCode===0}async function p(e,t){let n=await e.format(t,`#{pane_pid}`),r=Number(n);if(!Number.isInteger(r)||r<=0)throw Error(`tmux did not report a pane pid for ${t}`);return r}async function m(e,t,n){for(;;){if(a.default.existsSync(n))return null;let r=await _(e,t);if(r.exited)return r.code;await b(100)}}async function h(e,t,n){let r=Date.now()+2e3;for(;Date.now()<r;){if(a.default.existsSync(n)||await e.sessionMissing(t))return;await b(25)}}async function g(e,t,n){let r=Date.now()+n;for(;Date.now()<r;){if((await _(e,t)).exited)return!0;await b(100)}return!1}async function _(e,t){let n=await e.format(t,`#{pane_dead}:#{pane_dead_status}:#{session_name}`);if(n===void 0)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}}function v(e,t){try{return process.kill(e,`SIGTERM`),`signalled`}catch(e){return e.code===`ESRCH`?`gone`:(process.emitWarning(`Could not signal tmux target ${t}: ${x(e)}`),`failed`)}}function y(e,t){let n;try{let r=a.default.statSync(e).size,i=Math.min(r,t),o=Buffer.alloc(i);return n=a.default.openSync(e,`r`),a.default.readSync(n,o,0,i,r-i),o.toString(`utf8`)}catch(t){return t.code!==`ENOENT`&&process.emitWarning(`Could not read tmux log ${e}: ${x(t)}`),``}finally{n!==void 0&&a.default.closeSync(n)}}function b(e){return new Promise(t=>{setTimeout(t,e).unref?.()})}function x(e){return e instanceof Error?e.message:String(e)}exports.TMUX_TARGET_PREFIX=`doom-tmux-`,exports.TmuxBackend=l;
2
+ //# sourceMappingURL=TmuxBackend.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TmuxBackend.cjs","names":["TmuxClient","shellJoin","runtimeEntry","getResultMaxBytes","readExitMetadata","supervisorPaths","path","getLogMaxBytes","createHash","NO_TERMINAL_INPUT_ENV","fs"],"sources":["../../../../src/adapters/TmuxBackend/TmuxBackend.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n type CommandSpec,\n cleanupSupervisorFiles,\n NO_TERMINAL_INPUT_ENV,\n readExitMetadata,\n runtimeEntry,\n type SupervisorPaths,\n shellJoin,\n supervisorPaths,\n writeCommandSpec,\n} from '../../schemas/runnerSpec.ts';\nimport { getLogMaxBytes, getResultMaxBytes } from '../../types/config.ts';\nimport type { RunHandle } from '../../types/launcher';\nimport type { PtyRun } from '../../types/ptyHost';\nimport type { IRunnerPaths } from '../../services/RunnerPaths/types';\nimport type { ExitResult } from '../../types/spawner';\nimport type { IRmuxBackend, RmuxLaunchRequest } from '../../types/rmuxBackend';\nimport type { ITmuxClient } from '../../types/tmuxClient';\nimport { TmuxClient } from './TmuxClient.ts';\n\nconst POLL_MS = 100;\nconst LOG_DRAIN_TIMEOUT_MS = 2_000;\nconst LOG_DRAIN_POLL_MS = 25;\n/** Pane liveness, its exit status, and whether the session still exists, in one read. */\nconst PANE_STATE_FORMAT = '#{pane_dead}:#{pane_dead_status}:#{session_name}';\nconst STOP_GRACE_MS = 3_000;\n/** Distinct from the RMUX prefix so a target names the backend that owns it. */\nconst SESSION_PREFIX = 'doom-tmux-';\nconst OWNED_TARGET_PATTERN = /^doom-tmux-[A-Za-z0-9_-]+$/;\nexport const TMUX_TARGET_PREFIX = SESSION_PREFIX;\nconst SOCKET_HASH_LENGTH = 12;\nconst SOCKET_PREFIX = 'doom-tmux-';\nconst NON_INTERACTIVE_ENV = { NO_COLOR: '1', CI: '1' };\n\n/**\n * Supervises runs in tmux panes, for hosts with no compatible RMUX binary.\n *\n * The pane runs the same runnerHost supervisor as the RMUX backend, so exit\n * metadata, log rotation, and lifeline handling are shared rather than\n * reimplemented: only the multiplexer differs. Kept beside the RMUX backend\n * rather than folded into it because that path has no unit coverage to\n * refactor against.\n */\nexport class TmuxBackend implements IRmuxBackend {\n private clientPromise: Promise<ITmuxClient | undefined> | undefined;\n private readonly runs = new Map<string, PtyRun>();\n private readonly completions = new Map<string, Promise<ExitResult>>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly createClient: (socket: string) => ITmuxClient = (socket) => new TmuxClient(socket),\n ) {}\n\n async launch(request: RmuxLaunchRequest): Promise<RunHandle | undefined> {\n const tmux = await this.client();\n if (!tmux) 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 // One quoted command rather than an argv: tmux takes shell-command as a\n // single word, and quoting here is what keeps a path with spaces intact.\n const supervisor = shellJoin([process.execPath, runtimeEntry('runnerHost'), aux.spec, aux.gate, aux.exit]);\n await this.checked(tmux, ['new-session', '-d', '-s', target, '-c', request.cwd, supervisor]);\n created = true;\n // Set before the gate opens, so it is in force by the time the command\n // can exit. A pane that vanishes on exit takes the log sink down with it\n // before Node has finished starting, losing the output of fast commands,\n // and leaves #{pane_dead} with nothing to report.\n await this.checked(tmux, ['set-option', '-t', target, 'remain-on-exit', 'on']);\n await this.checked(tmux, ['pipe-pane', '-t', target, this.logPipeCommand(logPath, aux.logDone)]);\n\n const pid = await panePid(tmux, target);\n fs.writeFileSync(aux.gate, '', { mode: 0o600 });\n released = true;\n\n let buffering = true;\n const completion = this.monitor(tmux, target, aux);\n this.trackCompletion(target, completion);\n const handle: RunHandle = {\n id: request.id,\n name: request.name,\n pid,\n logPath,\n backend: 'tmux',\n backendTarget: target,\n output: () => (buffering ? readTail(logPath, getResultMaxBytes() * 2) : ''),\n completion: () => completion,\n detach: () => {\n buffering = false;\n },\n stop: () => this.stop(target, pid),\n };\n\n if (request.interactive) this.runs.set(request.name, this.interactiveRun(handle, tmux, target, completion));\n void completion\n .finally(() => this.runs.delete(request.name))\n .catch((error: unknown) => process.emitWarning(`tmux completion failed for ${target}: ${errorMessage(error)}`));\n return handle;\n } catch (error) {\n if (released) throw error;\n if (created) await killSession(tmux, target);\n cleanupSupervisorFiles(aux);\n process.emitWarning(\n request.interactive\n ? `tmux launch unavailable for interactive runner: ${errorMessage(error)}`\n : `tmux 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 tmux = await this.client();\n if (!tmux) return false;\n try {\n if (await tmux.sessionMissing(target)) return true;\n\n const pid = await panePid(tmux, 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(tmux, target, STOP_GRACE_MS)) return true;\n if (await tmux.sessionMissing(target)) return true;\n\n // Revalidate after the grace period so a replacement pane is never closed.\n const currentPid = await panePid(tmux, target).catch(() => undefined);\n if (currentPid !== expectedPid) return false;\n\n return killSession(tmux, target);\n } catch (error) {\n process.emitWarning(`Could not stop tmux target ${target}: ${errorMessage(error)}`);\n return false;\n }\n }\n\n async watch(id: string, target: string, sessionId?: string): Promise<ExitResult | undefined> {\n const tmux = await this.client();\n if (!tmux) return undefined;\n if (await tmux.sessionMissing(target)) return undefined;\n const completion = this.monitor(tmux, 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 tmux = await this.client();\n if (!tmux) return false;\n const result = await tmux.run(['send-keys', '-t', target, '-l', text]).catch((error: unknown) => {\n process.emitWarning(`Could not send input to tmux target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n return result?.returnCode === 0;\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(`tmux monitor failed for ${target}: ${errorMessage(error)}`));\n }\n\n private client(): Promise<ITmuxClient | undefined> {\n this.clientPromise ??= this.resolveClient();\n return this.clientPromise;\n }\n\n private async resolveClient(): Promise<ITmuxClient | undefined> {\n const client = this.createClient(socketName(this.paths.repositoryPath()));\n try {\n const version = await client.run(['-V']);\n if (version.returnCode !== 0) throw new Error(version.stderr.trim() || 'tmux reported no version');\n return client;\n } catch (error) {\n process.emitWarning(`tmux is unavailable: ${errorMessage(error)}`);\n return undefined;\n }\n }\n\n private async checked(tmux: ITmuxClient, args: readonly string[]): Promise<void> {\n const result = await tmux.run(args);\n if (result.returnCode !== 0) {\n throw new Error(`tmux ${args[0] ?? ''} failed: ${result.stderr.trim() || `exit ${result.returnCode}`}`);\n }\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 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 private logPipeCommand(logPath: string, donePath: string): string {\n return shellJoin([\n process.execPath,\n runtimeEntry('logSink'),\n logPath,\n this.paths.rotatedLogPathFor(path.basename(logPath, '.log')),\n String(getLogMaxBytes()),\n donePath,\n ]);\n }\n\n private async monitor(tmux: ITmuxClient, target: string, aux: SupervisorPaths): Promise<ExitResult> {\n try {\n const paneCode = await waitForRunEnd(tmux, target, aux.exit);\n await waitForLogDrain(tmux, target, aux.logDone);\n return readExitMetadata(aux.exit) ?? { code: paneCode, signal: null };\n } finally {\n if (!(await tmux.sessionMissing(target).catch(() => false))) await killSession(tmux, target);\n cleanupSupervisorFiles(aux);\n }\n }\n\n private interactiveRun(\n handle: RunHandle,\n tmux: ITmuxClient,\n target: string,\n completion: Promise<ExitResult>,\n ): 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 captured = await tmux.run(['capture-pane', '-p', '-t', target]).catch((error: unknown) => {\n process.emitWarning(`Could not capture tmux screen for ${handle.name}: ${errorMessage(error)}`);\n return undefined;\n });\n const next = captured?.returnCode === 0 ? captured.stdout : screen;\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(`tmux interactive completion failed for ${handle.name}: ${errorMessage(error)}`),\n );\n return {\n ...handle,\n write: (text) => {\n void tmux\n .run(['send-keys', '-t', target, '-l', text])\n .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 tmux\n .run(['resize-window', '-t', target, '-x', String(cols), '-y', String(rows)])\n .catch((error: unknown) => process.emitWarning(errorMessage(error)));\n },\n };\n }\n}\n\nfunction socketName(repositoryPath: string): string {\n const hash = createHash('sha256').update(repositoryPath).digest('hex').slice(0, SOCKET_HASH_LENGTH);\n return `${SOCKET_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 ...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 killSession(tmux: ITmuxClient, target: string): Promise<boolean> {\n const result = await tmux.run(['kill-session', '-t', target]).catch((error: unknown) => {\n process.emitWarning(`Could not close tmux target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n return result?.returnCode === 0;\n}\n\nasync function panePid(tmux: ITmuxClient, target: string): Promise<number> {\n const message = await tmux.format(target, '#{pane_pid}');\n const pid = Number(message);\n if (!Number.isInteger(pid) || pid <= 0) throw new Error(`tmux 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 pane can end and outlives the server the\n * pane's exit takes down, so asking tmux is the fallback for a supervisor that\n * died without recording anything.\n */\nasync function waitForRunEnd(tmux: ITmuxClient, target: string, exitPath: string): Promise<number | null> {\n for (;;) {\n if (fs.existsSync(exitPath)) return null;\n const state = await readPaneExit(tmux, target);\n if (state.exited) return state.code;\n await delay(POLL_MS);\n }\n}\n\nasync function waitForLogDrain(tmux: ITmuxClient, 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 tmux.sessionMissing(target)) return;\n await delay(LOG_DRAIN_POLL_MS);\n }\n}\n\nasync function waitForPaneStop(tmux: ITmuxClient, target: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if ((await readPaneExit(tmux, target)).exited) return true;\n await delay(POLL_MS);\n }\n return false;\n}\n\n/** One formatted read answers both questions this poll asks. */\nasync function readPaneExit(tmux: ITmuxClient, target: string): Promise<{ exited: boolean; code: number | null }> {\n const message = await tmux.format(target, PANE_STATE_FORMAT);\n // An unreachable server is the ordinary end of the last run: the pane exiting\n // ends its session, and the last session ending stops the server.\n if (message === undefined) return { exited: true, code: null };\n const [dead, status, ...rest] = message.trim().split(':');\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\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 tmux 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 tmux 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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"}
@@ -0,0 +1,2 @@
1
+ import{getLogMaxBytes as e,getResultMaxBytes as t}from"../../types/config.mjs";import{NO_TERMINAL_INPUT_ENV as n,cleanupSupervisorFiles as r,readExitMetadata as i,runtimeEntry as a,shellJoin as o,supervisorPaths as s,writeCommandSpec as c}from"../../schemas/runnerSpec.mjs";import{TmuxClient as l}from"./TmuxClient.mjs";import{createHash as u}from"node:crypto";import d from"node:fs";import f from"node:path";const p=`doom-tmux-`,m=/^doom-tmux-[A-Za-z0-9_-]+$/,h=p,g={NO_COLOR:`1`,CI:`1`};var _=class{paths;createClient;clientPromise;runs=new Map;completions=new Map;constructor(e,t=e=>new l(e)){this.paths=e,this.createClient=t}async launch(e){let n=await this.client();if(!n)return;this.paths.ensureDirectories();let i=`${p}${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 r=o([process.execPath,a(`runnerHost`),c.spec,c.gate,c.exit]);await this.checked(n,[`new-session`,`-d`,`-s`,i,`-c`,e.cwd,r]),l=!0,await this.checked(n,[`set-option`,`-t`,i,`remain-on-exit`,`on`]),await this.checked(n,[`pipe-pane`,`-t`,i,this.logPipeCommand(s,c.logDone)]);let f=await x(n,i);d.writeFileSync(c.gate,``,{mode:384}),u=!0;let p=!0,m=this.monitor(n,i,c);this.trackCompletion(i,m);let h={id:e.id,name:e.name,pid:f,logPath:s,backend:`tmux`,backendTarget:i,output:()=>p?D(s,t()*2):``,completion:()=>m,detach:()=>{p=!1},stop:()=>this.stop(i,f)};return e.interactive&&this.runs.set(e.name,this.interactiveRun(h,n,i,m)),m.finally(()=>this.runs.delete(e.name)).catch(e=>process.emitWarning(`tmux completion failed for ${i}: ${k(e)}`)),h}catch(t){if(u)throw t;l&&await b(n,i),r(c),process.emitWarning(e.interactive?`tmux launch unavailable for interactive runner: ${k(t)}`:`tmux launch unavailable, using supervised subprocess: ${k(t)}`);return}}async stop(e,t){if(!m.test(e)||!Number.isInteger(t)||t<=0)return!1;let n=await this.client();if(!n)return!1;try{if(await n.sessionMissing(e))return!0;let r=await x(n,e);if(r!==t)return!1;let i=E(r,e);return i===`gone`?!0:i===`failed`?!1:await w(n,e,3e3)||await n.sessionMissing(e)?!0:await x(n,e).catch(()=>void 0)===t&&b(n,e)}catch(t){return process.emitWarning(`Could not stop tmux target ${e}: ${k(t)}`),!1}}async watch(e,t,n){let r=await this.client();if(!r||await r.sessionMissing(t))return;let i=this.monitor(r,t,this.auxPaths(e,n));return this.trackCompletion(t,i),i}async input(e,t){let n=await this.client();return n?(await n.run([`send-keys`,`-t`,e,`-l`,t]).catch(t=>{process.emitWarning(`Could not send input to tmux target ${e}: ${k(t)}`)}))?.returnCode===0:!1}get(e){return this.runs.get(e)}readOutcome(e,t){return i(this.auxPaths(e,t).exit)}trackCompletion(e,t){this.completions.set(e,t),t.finally(()=>this.completions.delete(e)).catch(t=>process.emitWarning(`tmux monitor failed for ${e}: ${k(t)}`))}client(){return this.clientPromise??=this.resolveClient(),this.clientPromise}async resolveClient(){let e=this.createClient(v(this.paths.repositoryPath()));try{let t=await e.run([`-V`]);if(t.returnCode!==0)throw Error(t.stderr.trim()||`tmux reported no version`);return e}catch(e){process.emitWarning(`tmux is unavailable: ${k(e)}`);return}}async checked(e,t){let n=await e.run(t);if(n.returnCode!==0)throw Error(`tmux ${t[0]??``} failed: ${n.stderr.trim()||`exit ${n.returnCode}`}`)}prepareFiles(e,t,n){r(n),d.writeFileSync(t,``,{mode:384});let i=y(e.sessionId,e.interactive),a={command:e.command,cwd:e.cwd,env:i};c(n.spec,a)}auxPaths(e,t){return s(this.paths.stateDirectory(t),e)}logPipeCommand(t,n){return o([process.execPath,a(`logSink`),t,this.paths.rotatedLogPathFor(f.basename(t,`.log`)),String(e()),n])}async monitor(e,t,n){try{let r=await S(e,t,n.exit);return await C(e,t,n.logDone),i(n.exit)??{code:r,signal:null}}finally{await e.sessionMissing(t).catch(()=>!1)||await b(e,t),r(n)}}interactiveRun(e,t,n,r){let i=``,a=!0,o=new Set;return(async()=>{for(;a;){let r=await t.run([`capture-pane`,`-p`,`-t`,n]).catch(t=>{process.emitWarning(`Could not capture tmux screen for ${e.name}: ${k(t)}`)}),a=r?.returnCode===0?r.stdout:i;if(a!==i){i=a;for(let e of o)e(a)}await O(100)}})(),r.finally(()=>{a=!1}).catch(t=>process.emitWarning(`tmux interactive completion failed for ${e.name}: ${k(t)}`)),{...e,write:e=>{t.run([`send-keys`,`-t`,n,`-l`,e]).catch(e=>process.emitWarning(k(e)))},screen:()=>i,onData:e=>(o.add(e),()=>o.delete(e)),resize:(e,r)=>{t.run([`resize-window`,`-t`,n,`-x`,String(e),`-y`,String(r)]).catch(e=>process.emitWarning(k(e)))}}}};function v(e){return`doom-tmux-${u(`sha256`).update(e).digest(`hex`).slice(0,12)}`}function y(e,t){let r={...process.env,...t?{}:g,...n,PI_SESSION_ID:e};return t||delete r.FORCE_COLOR,Object.fromEntries(Object.entries(r).filter(e=>e[1]!==void 0))}async function b(e,t){return(await e.run([`kill-session`,`-t`,t]).catch(e=>{process.emitWarning(`Could not close tmux target ${t}: ${k(e)}`)}))?.returnCode===0}async function x(e,t){let n=await e.format(t,`#{pane_pid}`),r=Number(n);if(!Number.isInteger(r)||r<=0)throw Error(`tmux did not report a pane pid for ${t}`);return r}async function S(e,t,n){for(;;){if(d.existsSync(n))return null;let r=await T(e,t);if(r.exited)return r.code;await O(100)}}async function C(e,t,n){let r=Date.now()+2e3;for(;Date.now()<r;){if(d.existsSync(n)||await e.sessionMissing(t))return;await O(25)}}async function w(e,t,n){let r=Date.now()+n;for(;Date.now()<r;){if((await T(e,t)).exited)return!0;await O(100)}return!1}async function T(e,t){let n=await e.format(t,`#{pane_dead}:#{pane_dead_status}:#{session_name}`);if(n===void 0)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}}function E(e,t){try{return process.kill(e,`SIGTERM`),`signalled`}catch(e){return e.code===`ESRCH`?`gone`:(process.emitWarning(`Could not signal tmux target ${t}: ${k(e)}`),`failed`)}}function D(e,t){let n;try{let r=d.statSync(e).size,i=Math.min(r,t),a=Buffer.alloc(i);return n=d.openSync(e,`r`),d.readSync(n,a,0,i,r-i),a.toString(`utf8`)}catch(t){return t.code!==`ENOENT`&&process.emitWarning(`Could not read tmux log ${e}: ${k(t)}`),``}finally{n!==void 0&&d.closeSync(n)}}function O(e){return new Promise(t=>{setTimeout(t,e).unref?.()})}function k(e){return e instanceof Error?e.message:String(e)}export{h as TMUX_TARGET_PREFIX,_ as TmuxBackend};
2
+ //# sourceMappingURL=TmuxBackend.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TmuxBackend.mjs","names":[],"sources":["../../../../src/adapters/TmuxBackend/TmuxBackend.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n type CommandSpec,\n cleanupSupervisorFiles,\n NO_TERMINAL_INPUT_ENV,\n readExitMetadata,\n runtimeEntry,\n type SupervisorPaths,\n shellJoin,\n supervisorPaths,\n writeCommandSpec,\n} from '../../schemas/runnerSpec.ts';\nimport { getLogMaxBytes, getResultMaxBytes } from '../../types/config.ts';\nimport type { RunHandle } from '../../types/launcher';\nimport type { PtyRun } from '../../types/ptyHost';\nimport type { IRunnerPaths } from '../../services/RunnerPaths/types';\nimport type { ExitResult } from '../../types/spawner';\nimport type { IRmuxBackend, RmuxLaunchRequest } from '../../types/rmuxBackend';\nimport type { ITmuxClient } from '../../types/tmuxClient';\nimport { TmuxClient } from './TmuxClient.ts';\n\nconst POLL_MS = 100;\nconst LOG_DRAIN_TIMEOUT_MS = 2_000;\nconst LOG_DRAIN_POLL_MS = 25;\n/** Pane liveness, its exit status, and whether the session still exists, in one read. */\nconst PANE_STATE_FORMAT = '#{pane_dead}:#{pane_dead_status}:#{session_name}';\nconst STOP_GRACE_MS = 3_000;\n/** Distinct from the RMUX prefix so a target names the backend that owns it. */\nconst SESSION_PREFIX = 'doom-tmux-';\nconst OWNED_TARGET_PATTERN = /^doom-tmux-[A-Za-z0-9_-]+$/;\nexport const TMUX_TARGET_PREFIX = SESSION_PREFIX;\nconst SOCKET_HASH_LENGTH = 12;\nconst SOCKET_PREFIX = 'doom-tmux-';\nconst NON_INTERACTIVE_ENV = { NO_COLOR: '1', CI: '1' };\n\n/**\n * Supervises runs in tmux panes, for hosts with no compatible RMUX binary.\n *\n * The pane runs the same runnerHost supervisor as the RMUX backend, so exit\n * metadata, log rotation, and lifeline handling are shared rather than\n * reimplemented: only the multiplexer differs. Kept beside the RMUX backend\n * rather than folded into it because that path has no unit coverage to\n * refactor against.\n */\nexport class TmuxBackend implements IRmuxBackend {\n private clientPromise: Promise<ITmuxClient | undefined> | undefined;\n private readonly runs = new Map<string, PtyRun>();\n private readonly completions = new Map<string, Promise<ExitResult>>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly createClient: (socket: string) => ITmuxClient = (socket) => new TmuxClient(socket),\n ) {}\n\n async launch(request: RmuxLaunchRequest): Promise<RunHandle | undefined> {\n const tmux = await this.client();\n if (!tmux) 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 // One quoted command rather than an argv: tmux takes shell-command as a\n // single word, and quoting here is what keeps a path with spaces intact.\n const supervisor = shellJoin([process.execPath, runtimeEntry('runnerHost'), aux.spec, aux.gate, aux.exit]);\n await this.checked(tmux, ['new-session', '-d', '-s', target, '-c', request.cwd, supervisor]);\n created = true;\n // Set before the gate opens, so it is in force by the time the command\n // can exit. A pane that vanishes on exit takes the log sink down with it\n // before Node has finished starting, losing the output of fast commands,\n // and leaves #{pane_dead} with nothing to report.\n await this.checked(tmux, ['set-option', '-t', target, 'remain-on-exit', 'on']);\n await this.checked(tmux, ['pipe-pane', '-t', target, this.logPipeCommand(logPath, aux.logDone)]);\n\n const pid = await panePid(tmux, target);\n fs.writeFileSync(aux.gate, '', { mode: 0o600 });\n released = true;\n\n let buffering = true;\n const completion = this.monitor(tmux, target, aux);\n this.trackCompletion(target, completion);\n const handle: RunHandle = {\n id: request.id,\n name: request.name,\n pid,\n logPath,\n backend: 'tmux',\n backendTarget: target,\n output: () => (buffering ? readTail(logPath, getResultMaxBytes() * 2) : ''),\n completion: () => completion,\n detach: () => {\n buffering = false;\n },\n stop: () => this.stop(target, pid),\n };\n\n if (request.interactive) this.runs.set(request.name, this.interactiveRun(handle, tmux, target, completion));\n void completion\n .finally(() => this.runs.delete(request.name))\n .catch((error: unknown) => process.emitWarning(`tmux completion failed for ${target}: ${errorMessage(error)}`));\n return handle;\n } catch (error) {\n if (released) throw error;\n if (created) await killSession(tmux, target);\n cleanupSupervisorFiles(aux);\n process.emitWarning(\n request.interactive\n ? `tmux launch unavailable for interactive runner: ${errorMessage(error)}`\n : `tmux 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 tmux = await this.client();\n if (!tmux) return false;\n try {\n if (await tmux.sessionMissing(target)) return true;\n\n const pid = await panePid(tmux, 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(tmux, target, STOP_GRACE_MS)) return true;\n if (await tmux.sessionMissing(target)) return true;\n\n // Revalidate after the grace period so a replacement pane is never closed.\n const currentPid = await panePid(tmux, target).catch(() => undefined);\n if (currentPid !== expectedPid) return false;\n\n return killSession(tmux, target);\n } catch (error) {\n process.emitWarning(`Could not stop tmux target ${target}: ${errorMessage(error)}`);\n return false;\n }\n }\n\n async watch(id: string, target: string, sessionId?: string): Promise<ExitResult | undefined> {\n const tmux = await this.client();\n if (!tmux) return undefined;\n if (await tmux.sessionMissing(target)) return undefined;\n const completion = this.monitor(tmux, 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 tmux = await this.client();\n if (!tmux) return false;\n const result = await tmux.run(['send-keys', '-t', target, '-l', text]).catch((error: unknown) => {\n process.emitWarning(`Could not send input to tmux target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n return result?.returnCode === 0;\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(`tmux monitor failed for ${target}: ${errorMessage(error)}`));\n }\n\n private client(): Promise<ITmuxClient | undefined> {\n this.clientPromise ??= this.resolveClient();\n return this.clientPromise;\n }\n\n private async resolveClient(): Promise<ITmuxClient | undefined> {\n const client = this.createClient(socketName(this.paths.repositoryPath()));\n try {\n const version = await client.run(['-V']);\n if (version.returnCode !== 0) throw new Error(version.stderr.trim() || 'tmux reported no version');\n return client;\n } catch (error) {\n process.emitWarning(`tmux is unavailable: ${errorMessage(error)}`);\n return undefined;\n }\n }\n\n private async checked(tmux: ITmuxClient, args: readonly string[]): Promise<void> {\n const result = await tmux.run(args);\n if (result.returnCode !== 0) {\n throw new Error(`tmux ${args[0] ?? ''} failed: ${result.stderr.trim() || `exit ${result.returnCode}`}`);\n }\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 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 private logPipeCommand(logPath: string, donePath: string): string {\n return shellJoin([\n process.execPath,\n runtimeEntry('logSink'),\n logPath,\n this.paths.rotatedLogPathFor(path.basename(logPath, '.log')),\n String(getLogMaxBytes()),\n donePath,\n ]);\n }\n\n private async monitor(tmux: ITmuxClient, target: string, aux: SupervisorPaths): Promise<ExitResult> {\n try {\n const paneCode = await waitForRunEnd(tmux, target, aux.exit);\n await waitForLogDrain(tmux, target, aux.logDone);\n return readExitMetadata(aux.exit) ?? { code: paneCode, signal: null };\n } finally {\n if (!(await tmux.sessionMissing(target).catch(() => false))) await killSession(tmux, target);\n cleanupSupervisorFiles(aux);\n }\n }\n\n private interactiveRun(\n handle: RunHandle,\n tmux: ITmuxClient,\n target: string,\n completion: Promise<ExitResult>,\n ): 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 captured = await tmux.run(['capture-pane', '-p', '-t', target]).catch((error: unknown) => {\n process.emitWarning(`Could not capture tmux screen for ${handle.name}: ${errorMessage(error)}`);\n return undefined;\n });\n const next = captured?.returnCode === 0 ? captured.stdout : screen;\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(`tmux interactive completion failed for ${handle.name}: ${errorMessage(error)}`),\n );\n return {\n ...handle,\n write: (text) => {\n void tmux\n .run(['send-keys', '-t', target, '-l', text])\n .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 tmux\n .run(['resize-window', '-t', target, '-x', String(cols), '-y', String(rows)])\n .catch((error: unknown) => process.emitWarning(errorMessage(error)));\n },\n };\n }\n}\n\nfunction socketName(repositoryPath: string): string {\n const hash = createHash('sha256').update(repositoryPath).digest('hex').slice(0, SOCKET_HASH_LENGTH);\n return `${SOCKET_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 ...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 killSession(tmux: ITmuxClient, target: string): Promise<boolean> {\n const result = await tmux.run(['kill-session', '-t', target]).catch((error: unknown) => {\n process.emitWarning(`Could not close tmux target ${target}: ${errorMessage(error)}`);\n return undefined;\n });\n return result?.returnCode === 0;\n}\n\nasync function panePid(tmux: ITmuxClient, target: string): Promise<number> {\n const message = await tmux.format(target, '#{pane_pid}');\n const pid = Number(message);\n if (!Number.isInteger(pid) || pid <= 0) throw new Error(`tmux 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 pane can end and outlives the server the\n * pane's exit takes down, so asking tmux is the fallback for a supervisor that\n * died without recording anything.\n */\nasync function waitForRunEnd(tmux: ITmuxClient, target: string, exitPath: string): Promise<number | null> {\n for (;;) {\n if (fs.existsSync(exitPath)) return null;\n const state = await readPaneExit(tmux, target);\n if (state.exited) return state.code;\n await delay(POLL_MS);\n }\n}\n\nasync function waitForLogDrain(tmux: ITmuxClient, 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 tmux.sessionMissing(target)) return;\n await delay(LOG_DRAIN_POLL_MS);\n }\n}\n\nasync function waitForPaneStop(tmux: ITmuxClient, target: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if ((await readPaneExit(tmux, target)).exited) return true;\n await delay(POLL_MS);\n }\n return false;\n}\n\n/** One formatted read answers both questions this poll asks. */\nasync function readPaneExit(tmux: ITmuxClient, target: string): Promise<{ exited: boolean; code: number | null }> {\n const message = await tmux.format(target, PANE_STATE_FORMAT);\n // An unreachable server is the ordinary end of the last run: the pane exiting\n // ends its session, and the last session ending stops the server.\n if (message === undefined) return { exited: true, code: null };\n const [dead, status, ...rest] = message.trim().split(':');\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\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 tmux 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 tmux 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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"}
@@ -0,0 +1,2 @@
1
+ let e=require("node:child_process");var t=class{socket;binary;constructor(e,t=`tmux`){this.socket=e,this.binary=t}run(t){return new Promise((n,r)=>{(0,e.execFile)(this.binary,[`-L`,this.socket,...t],{timeout:1e4,maxBuffer:4194304,encoding:`utf8`},(e,t,i)=>{let a=e?.code;if(e&&typeof a!=`number`){r(e);return}n({returnCode:typeof a==`number`?a:0,stdout:t,stderr:i})})})}async format(e,t){let n=await this.run([`display-message`,`-p`,`-t`,e,`-F`,t]).catch(()=>void 0);if(!(!n||n.returnCode!==0))return n.stdout.replace(/\n$/,``)}async sessionMissing(e){let t=await this.run([`has-session`,`-t`,e]).catch(()=>void 0);return!t||t.returnCode!==0}};exports.TmuxClient=t;
2
+ //# sourceMappingURL=TmuxClient.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TmuxClient.cjs","names":[],"sources":["../../../../src/adapters/TmuxBackend/TmuxClient.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport type { ITmuxClient, TmuxResult } from '../../types/tmuxClient';\n\nconst TMUX_BINARY = 'tmux';\nconst SOCKET_OPTION = '-L';\n/** Long enough for a loaded server, short enough that a hung tmux is not fatal. */\nconst COMMAND_TIMEOUT_MS = 10_000;\nconst MAX_OUTPUT_BYTES = 4 * 1024 * 1024;\n\nexport class TmuxClient implements ITmuxClient {\n constructor(\n private readonly socket: string,\n private readonly binary = TMUX_BINARY,\n ) {}\n\n run(args: readonly string[]): Promise<TmuxResult> {\n return new Promise((resolve, reject) => {\n execFile(\n this.binary,\n [SOCKET_OPTION, this.socket, ...args],\n { timeout: COMMAND_TIMEOUT_MS, maxBuffer: MAX_OUTPUT_BYTES, encoding: 'utf8' },\n (error, stdout, stderr) => {\n // A non-zero tmux exit is an answer, not a fault: `has-session`\n // reports absence that way. Only a missing or hung binary rejects.\n const code = (error as (Error & { code?: number | string }) | null)?.code;\n if (error && typeof code !== 'number') {\n reject(error);\n return;\n }\n resolve({ returnCode: typeof code === 'number' ? code : 0, stdout, stderr });\n },\n );\n });\n }\n\n async format(target: string, format: string): Promise<string | undefined> {\n const result = await this.run(['display-message', '-p', '-t', target, '-F', format]).catch(() => undefined);\n if (!result || result.returnCode !== 0) return undefined;\n return result.stdout.replace(/\\n$/, '');\n }\n\n async sessionMissing(target: string): Promise<boolean> {\n const result = await this.run(['has-session', '-t', target]).catch(() => undefined);\n return !result || result.returnCode !== 0;\n }\n}\n"],"mappings":"oCASA,IAAa,EAAb,KAA+C,CAE1B,OACA,OAFnB,YACE,EACA,EAA0B,OAC1B,CAFiB,KAAA,OAAA,EACA,KAAA,OAAA,CAChB,CAEH,IAAI,EAA8C,CAChD,OAAO,IAAI,SAAS,EAAS,IAAW,EACtC,EAAA,EAAA,SAAA,CACE,KAAK,OACL,CAAC,KAAe,KAAK,OAAQ,GAAG,CAAI,EACpC,CAAE,QAAS,IAAoB,UAAW,QAAkB,SAAU,MAAO,GAC5E,EAAO,EAAQ,IAAW,CAGzB,IAAM,EAAQ,GAAuD,KACrE,GAAI,GAAS,OAAO,GAAS,SAAU,CACrC,EAAO,CAAK,EACZ,MACF,CACA,EAAQ,CAAE,WAAY,OAAO,GAAS,SAAW,EAAO,EAAG,SAAQ,QAAO,CAAC,CAC7E,CACF,CACF,CAAC,CACH,CAEA,MAAM,OAAO,EAAgB,EAA6C,CACxE,IAAM,EAAS,MAAM,KAAK,IAAI,CAAC,kBAAmB,KAAM,KAAM,EAAQ,KAAM,CAAM,CAAC,CAAC,CAAC,UAAY,IAAA,EAAS,EACtG,MAAC,GAAU,EAAO,aAAe,GACrC,OAAO,EAAO,OAAO,QAAQ,MAAO,EAAE,CACxC,CAEA,MAAM,eAAe,EAAkC,CACrD,IAAM,EAAS,MAAM,KAAK,IAAI,CAAC,cAAe,KAAM,CAAM,CAAC,CAAC,CAAC,UAAY,IAAA,EAAS,EAClF,MAAO,CAAC,GAAU,EAAO,aAAe,CAC1C,CACF"}