@agimon-ai/doompi-runner 0.0.1-alpha.24 → 0.0.1-alpha.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/adapters/pi/extension.cjs +2 -2
- package/dist/src/adapters/pi/extension.cjs.map +1 -1
- package/dist/src/adapters/pi/extension.d.cts.map +1 -1
- package/dist/src/adapters/pi/extension.d.mts +1 -1
- package/dist/src/adapters/pi/extension.d.mts.map +1 -1
- package/dist/src/adapters/pi/extension.mjs +2 -2
- package/dist/src/adapters/pi/extension.mjs.map +1 -1
- package/dist/src/commands/bash/bashTool.cjs +1 -1
- package/dist/src/commands/bash/bashTool.cjs.map +1 -1
- package/dist/src/commands/bash/bashTool.d.cts +1 -1
- package/dist/src/commands/bash/bashTool.d.cts.map +1 -1
- package/dist/src/commands/bash/bashTool.d.mts +1 -1
- package/dist/src/commands/bash/bashTool.d.mts.map +1 -1
- package/dist/src/commands/bash/bashTool.mjs +1 -1
- package/dist/src/commands/bash/bashTool.mjs.map +1 -1
- package/dist/src/schemas/runnerSpec.cjs +1 -1
- package/dist/src/schemas/runnerSpec.cjs.map +1 -1
- package/dist/src/schemas/runnerSpec.mjs +1 -1
- package/dist/src/schemas/runnerSpec.mjs.map +1 -1
- package/dist/src/services/runs/compaction.cjs +1 -1
- package/dist/src/services/runs/compaction.cjs.map +1 -1
- package/dist/src/services/runs/compaction.d.cts +1 -1
- package/dist/src/services/runs/compaction.d.cts.map +1 -1
- package/dist/src/services/runs/compaction.d.mts +1 -1
- package/dist/src/services/runs/compaction.d.mts.map +1 -1
- package/dist/src/services/runs/compaction.mjs +1 -1
- package/dist/src/services/runs/compaction.mjs.map +1 -1
- package/package.json +9 -14
- package/dist/ptyProvider.cjs +0 -1
- package/dist/ptyProvider.d.cts +0 -2
- package/dist/ptyProvider.d.mts +0 -2
- package/dist/ptyProvider.mjs +0 -1
- package/dist/src/adapters/pty/ptyProvider.cjs +0 -2
- package/dist/src/adapters/pty/ptyProvider.cjs.map +0 -1
- package/dist/src/adapters/pty/ptyProvider.d.cts +0 -12
- package/dist/src/adapters/pty/ptyProvider.d.cts.map +0 -1
- package/dist/src/adapters/pty/ptyProvider.d.mts +0 -12
- package/dist/src/adapters/pty/ptyProvider.d.mts.map +0 -1
- package/dist/src/adapters/pty/ptyProvider.mjs +0 -2
- package/dist/src/adapters/pty/ptyProvider.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ installation:
|
|
|
26
26
|
pi install npm:@agimon-ai/doompi-runner
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Pi discovers Runner's sole extension factory from `package.json.pi.extensions`. The factory
|
|
29
|
+
Pi discovers Runner's sole extension factory from `package.json.pi.extensions`. The factory joins the runner-scoped Cordis host, mounts one Runner plugin fiber, and disposes that fiber before releasing its host lease when the Pi session shuts down.
|
|
30
30
|
|
|
31
31
|
## Use the `bash` tool
|
|
32
32
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
const e=require("../../services/runs/compaction.cjs"),t=require("../../types/config.cjs"),n=require("../../container/index.cjs"),r=require("
|
|
2
|
-
`);_.sendMessage({customType:`doom-runner-finished`,content:o,display:!0},{triggerTurn:!0,deliverAs:`steer`}),j&&
|
|
1
|
+
const e=require("../../services/runs/compaction.cjs"),t=require("../../types/config.cjs"),n=require("../../container/index.cjs"),r=require("../../services/runs/reconcile.cjs"),i=require("../../commands/bash/bashTool.cjs"),a=require("../../tui/format.cjs"),o=require("../../tui/runnerSpace.cjs");let s=require("@agimon-ai/doompi-extension-contracts/child-process"),c=require("@agimon-ai/doompi-extension-contracts/cordis-host"),l=require("@agimon-ai/doompi-extension-contracts/readiness"),u=require("@agimon-ai/doompi-extension-contracts/ui-hub"),d=require("@agimon-ai/doompi-telemetry");const f=`@agimon-ai/doompi-runner`,p=`runners`,m=`completed`,h=`stopped`,g=`doom-runner-runners`;function _(c,_){let v=n.createRunnerContainer(),y=v.runnerRegistry,ee=async()=>{try{y.close()}catch(e){process.emitWarning(`Could not close a partially installed doom-runner registry: ${String(e)}`)}};c.effect(()=>()=>ee(),`${f}/runtime`);let b=v.launcher,x=v.rmuxBackend,te=v.logReader,S=v.ptyHost,ne=v.bashRunService,C=v.paths,w=v.processControl,T=v.lifeline,E=!0,D=0,O,k,A=[],j,M,N,P=!1,F=!1,I,L,R,z,B,V=Promise.resolve(),re,H=!1,U=!1,W,G=new Set,K=new Set,q=new Set,J=e=>(q.add(e),e.then(()=>q.delete(e),()=>q.delete(e)),e),Y=async()=>{if(await V,!E||!P||!O)throw Error(`doom-runner requires an active Pi session`)},ie={run:async e=>(await Y(),J(ne.run(e)))},X=(e,t=O)=>E&&!F&&e===D&&t===O,Z=e=>(N??=(0,d.createDoomTelemetry)({serviceName:`doom-runner`,packageName:`@agimon-ai/doompi-runner`,cwd:e.cwd,env:process.env,enableLogs:!0,enableTraces:!0}),N),ae=async e=>{let t=O,n=D;if(!t||!X(n,t))return;let i=await y.list();if(!X(n,t))return;let o=i;if(e){let e=await r.reconcileActiveRunners({registry:y,launcher:b,rmuxBackend:x,processControl:w,currentHostPid:process.pid,startup:!1,active:i});for(let t of e.errors)process.emitWarning(t);if(e.reclaimed.length>0){let t=new Set(e.reclaimed);o=i.filter(e=>!t.has(e.id))}}if(!X(n,t))return;A=o.filter(e=>e.sessionId===t);let s=(k?o.filter(e=>(e.rootSessionId??e.sessionId)===k):[]).length;W!==s&&(W=s,M?.update(a.formatRunnerFooterContribution(s)),j?.hasUI&&j.ui.setStatus(g,a.formatRunnerStatus(s)));let c=[...K],l=await Promise.all(c.map(e=>y.get(e,t)));if(X(n,t))for(let[e,n]of l.entries()){let r=c[e];if(!r)continue;if(!n||n.sessionId!==t){K.delete(r);continue}if(n.state!==m||(K.delete(n.id),G.has(n.id)))continue;G.add(n.id);let i=n.exit?.reason??m,a=n.exit?.code===null||n.exit?.code===void 0?``:`, exit code ${n.exit.code}`,o=[`Background runner ${n.name} exited: ${i}${a}.`,`Runner ID: ${n.id}`,`Log: ${n.logPath}`,`Inspect: doom-runner logs ${n.id}`].join(`
|
|
2
|
+
`);_.sendMessage({customType:`doom-runner-finished`,content:o,display:!0},{triggerTurn:!0,deliverAs:`steer`}),j&&J(Z(j).recordEvent(`doom_runner.process_finished`,{outcome:`completed`,"runner.exit_code":n.exit?.code??0,"runner.backend":n.backend,...Number.isFinite(Date.parse(n.startedAt))?{duration_ms:Math.max(0,Date.now()-Date.parse(n.startedAt))}:{}}))}},Q=(e=!0)=>{if(!O||!P||F)return Promise.resolve();if(I)return H=!0,U||=e,I;let t=(async()=>{let t=e;do H=!1,U=!1,await ae(t),t=U;while(H&&!F)})().finally(()=>{I===t&&(I=void 0)});return I=t,t},oe=e=>{F||(j&&J(Z(j).recordError(`doom_runner.refresh_failed`,e)),process.emitWarning(`Could not refresh doom-runner: ${String(e)}`))},se=e=>{Q(e).catch(oe)},ce=()=>{R||L||F||(R=setImmediate(()=>{if(R=void 0,F)return;let e=(async()=>{let e=C.sweepHistoryAsync?await C.sweepHistoryAsync(t.getLogTtlMs()):C.sweepHistory(t.getLogTtlMs());for(let t of e.errors)process.emitWarning(t)})().catch(e=>process.emitWarning(`Could not sweep doom-runner history: ${String(e)}`)).finally(()=>{L===e&&(L=void 0)});L=e}),R.unref?.())},$=async(e,t)=>{try{await t()}catch(t){process.emitWarning(`Doom-runner cleanup could not ${e}: ${String(t)}`)}};ee=async()=>{if(F)return;E=!1,F=!0,D+=1,P=!1,z&&clearInterval(z),z=void 0,R&&clearImmediate(R),R=void 0;let e=B;B=void 0,e&&await $(`unsubscribe from the runner registry`,e);let t=re;re=void 0,t&&await $(`cancel standalone readiness work`,()=>t.dispose()),await Promise.allSettled(q);let n=I;n&&await $(`settle the runner refresh`,()=>n);let i=L;i&&await $(`settle runner history cleanup`,()=>i),await $(`dispose runner PTYs`,()=>S.disposeAll());let a=[];if(O)try{a=await y.listBySession(O)}catch(e){process.emitWarning(`Could not list runners during doom-runner shutdown: ${String(e)}`)}await Promise.all(a.map(async e=>{try{let t=await r.stopRunnerProcess(e,b,x);if(!t&&w.isAlive(e.pid)){process.emitWarning(`Could not stop runner ${e.id} during session shutdown`);return}await y.complete(e.id,{reason:h,code:null,signal:t?`SIGTERM`:null,stopReason:`session ended`},e.sessionId)}catch(t){process.emitWarning(`Could not clean up runner ${e.id}: ${String(t)}`)}}));let o=j,s=N;N=void 0,j=void 0,O=void 0,k=void 0,A=[],G.clear(),K.clear(),await Promise.all([$(`dispose the runner lifeline`,()=>T.dispose()),...o?.hasUI?[$(`clear the runner status`,()=>o.ui.setStatus(g,void 0))]:[]]),await $(`close the runner registry`,()=>y.close()),s&&(await $(`record the runner session finish`,()=>s.recordEvent(`doom_runner.session_finished`,{outcome:`stopped`})),await $(`stop runner telemetry`,()=>s.shutdown()))},c.inject([u.DOOM_UI_HUB_SERVICE],e=>{let t=(0,u.requireDoomUiHub)(e).registerFooter({source:f,id:`runner-count`,order:10});return M=t,()=>{t.dispose(),M===t&&(M=void 0)}}),B=y.subscribe(()=>se(!1)),z=setInterval(()=>se(!0),500),z.unref?.(),i.registerBashTool(_,{bashRunService:ie,getSessionId:async()=>{if(await Y(),!O)throw Error(`doom-runner requires an active Pi session`);return O},onRunnerStarted:e=>{!E||!P||(K.add(e),se(!1))}}),e.registerRunnerCompactionRecovery(_,{getSessionId:async()=>{try{return await Y(),O}catch{return}},listBySession:async e=>{try{await Y()}catch{return[]}return E?y.listBySession(e):[]}}),_.registerCommand(p,{description:`Open Runner Space: background processes started by bash`,handler:async(e,t)=>{if(!E)return;let n=D,r=O;if(r){if(!t.hasUI){t.ui.notify(`/runners requires interactive mode`,`error`);return}await Y(),await Q(),!(!r||!X(n,r))&&await o.openRunnerSpace(t,{getRunners:()=>X(n,r)?A:[],getPtyRun:e=>X(n,r)?x.get(e)??S.get(e):void 0,readLog:e=>X(n,r)?te.read(e,{lines:1e3}).text:``,stopRunner:async(e,t)=>{if(!X(n,r))return;let i=A.find(t=>t.id===e);i&&(i.backend===`rmux`&&i.backendTarget?await x.stop(i.backendTarget,i.pid):await b.stop(i.pid),X(n,r)&&(await y.complete(i.id,{reason:h,code:null,signal:null,stopReason:t},i.sessionId),X(n,r)&&await Q()))}})}}}),c.inject([u.DOOM_UI_HUB_SERVICE],e=>{let t=(0,u.requireDoomUiHub)(e).registerLeader({source:f,bindings:[{id:`runners.open`,path:[{key:`r`,label:`runners`,order:67},{key:`r`,label:`open`,detail:`background processes`}],command:{name:p}}]});return()=>t.dispose()}),_.on(`session_start`,(e,t)=>{if(!E)return;let n=t,i=n.sessionManager.getSessionId(),a=O,o=j,u=++D;O=i,k=(0,s.resolveRootSessionId)(i),j=n,P=!1,W=void 0,G.clear(),K.clear();let d=async e=>{let t=()=>!e.aborted&&X(u,i);if(!t())return[];if(a&&a!==i){await $(`dispose the previous session PTYs`,()=>S.disposeAll()),await $(`dispose the previous session lifeline`,()=>T.dispose()),o?.hasUI&&await $(`clear the previous session status`,()=>o.ui.setStatus(g,void 0));let e=N;if(N=void 0,e&&await $(`stop the previous session telemetry`,()=>e.shutdown()),!t())return[]}let s=Z(n);if(await s.recordEvent(`doom_runner.session_started`,{outcome:`started`}),!t()||(C.setSessionId(i),await T.arm(i),!t()))return[];let c=await y.listAll(i);if(!t())return[];for(let e of c)e.state===m?G.add(e.id):e.promoted&&K.add(e.id);let l=await r.cleanupLegacyRunnerStore({registry:y,launcher:b,rmuxBackend:x,processControl:w,currentHostPid:process.pid,paths:C});if(!t())return[];let d=await r.reconcileActiveRunners({registry:y,launcher:b,rmuxBackend:x,processControl:w,currentHostPid:process.pid,startup:!0});if(!t()||(P=!0,await Q(!1),!t()))return[];ce();let f=[...l.errors,...d.errors],p=l.reclaimed.length+d.reclaimed.length;return p>0&&n.hasUI&&n.ui.notify(`Reclaimed ${p} stale runner record(s)`,`warning`),await s.recordEvent(`doom_runner.reconciled`,{"runner.reclaimed_count":p,"runner.error_count":f.length,outcome:f.length===0?`completed`:`degraded`}),f},p=V.catch(()=>void 0),h=(async()=>{await p,X(u,i)&&await((0,l.readDoomReadinessCoordinator)(c)??(re??=(0,l.createDoomReadinessCoordinator)({notify:e=>{process.emitWarning(`${e.packageId} initialization ${e.state}: ${e.error?.message??e.diagnostics.join(`; `)}`)}}))).start(f,`${i}:${u}`,async e=>({value:void 0,diagnostics:await d(e)})).wait()})();V=h,J(h).catch(()=>void 0)})}async function v(e){let t=await(0,c.connectDoomCordisHost)(e,f),n=t.root.plugin(y,{pi:e});try{await n}catch(e){try{await n.dispose()}finally{await t.dispose()}throw e}let r;e.on(`session_shutdown`,()=>r??=(async()=>{try{await n.dispose()}finally{await t.dispose()}})())}function y(e,t){_(e,t.pi)}exports.default=v,exports.runnerExtension=v,exports.installRunnerRuntime=_;
|
|
3
3
|
//# sourceMappingURL=extension.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension.cjs","names":["createRunnerContainer","createDoomTelemetry","reconcileActiveRunners","formatRunnerFooterContribution","formatRunnerStatus","getLogTtlMs","stopRunnerProcess","registerDoomFooterContribution","openRunnerSpace","registerDoomLeaderContribution","resolveRootSessionId","provideRunnerPty","createProtocolRuntime","cleanupLegacyRunnerStore","Context"],"sources":["../../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport { registerDoomFooterContribution } from '@agimon-ai/doompi-ui/footer';\nimport { registerDoomLeaderContribution } from '@agimon-ai/doompi-ui/leader';\nimport { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { registerBashTool } from '../../commands/bash/bashTool.ts';\nimport { createRunnerContainer } from '../../container/index.ts';\nimport type { IBashRunService } from '../../types/bashRunService';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport { registerRunnerCompactionRecovery } from '../../services/runs/compaction.ts';\nimport { cleanupLegacyRunnerStore, reconcileActiveRunners, stopRunnerProcess } from '../../services/runs/reconcile.ts';\nimport { formatRunnerFooterContribution, formatRunnerStatus } from '../../tui/format.ts';\nimport { openRunnerSpace } from '../../tui/runnerSpace.ts';\nimport { getLogTtlMs } from '../../types/config.ts';\nimport { provideRunnerPty } from '../pty/ptyProvider.ts';\n\nconst LEADER_SOURCE = '@agimon-ai/doompi-runner';\n/** After doom-task's `t` (65) and before the core help group (70). */\nconst LEADER_GROUP_ORDER = 67;\nconst COMMAND_NAME = 'runners';\nconst ERR_REQUIRES_INTERACTIVE = '/runners requires interactive mode';\n\nconst SESSION_START_EVENT = 'session_start';\nconst SESSION_SHUTDOWN_EVENT = 'session_shutdown';\nconst RUNNER_FINISHED_MESSAGE = 'doom-runner-finished';\nconst COMPLETED_STATE = 'completed';\nconst RMUX_BACKEND = 'rmux';\nconst STOPPED_REASON = 'stopped';\nconst RUNNER_STATUS_KEY = 'doom-runner-runners';\nconst RUNNER_FOOTER_ORDER = 10;\nconst RUNNER_STATUS_POLL_MS = 500;\n\n/**\n * doom-runner: replaces pi's `bash` tool with a supervised one and provides a\n * CLI for anything it leaves running.\n */\nexport function installRunnerRuntime(cordis: Context, pi: ExtensionAPI): void {\n const container = createRunnerContainer();\n const registry = container.runnerRegistry;\n let disposeRuntime = async (): Promise<void> => {\n try {\n registry.close();\n } catch (error) {\n process.emitWarning(`Could not close a partially installed doom-runner registry: ${String(error)}`);\n }\n };\n cordis.effect(() => () => disposeRuntime(), `${LEADER_SOURCE}/runtime`);\n\n const launcher = container.launcher;\n const rmuxBackend = container.rmuxBackend;\n const logReader = container.logReader;\n const ptyHost = container.ptyHost;\n const bashRunService = container.bashRunService;\n const paths = container.paths;\n const processControl = container.processControl;\n const lifeline = container.lifeline;\n\n let active = true;\n let sessionGeneration = 0;\n let sessionId: string | undefined;\n let rootSessionId: string | undefined;\n let runners: RunnerRecord[] = [];\n let sessionContext: ExtensionContext | undefined;\n let disposePtyProvider: (() => void) | undefined;\n let disposeLeader: (() => void) | undefined;\n let footerContribution: ReturnType<typeof registerDoomFooterContribution> | undefined;\n let telemetry: DoomTelemetry | undefined;\n let sessionReady = false;\n let disposed = false;\n let refreshInFlight: Promise<void> | undefined;\n let historySweepInFlight: Promise<void> | undefined;\n let historySweepTimer: ReturnType<typeof setImmediate> | undefined;\n let statusPoll: ReturnType<typeof setInterval> | undefined;\n let unsubscribeRegistry: (() => void) | undefined;\n let lifecycleQueue: Promise<void> = Promise.resolve();\n let refreshQueued = false;\n let queuedReconciliation = false;\n let lastRunnerCount: number | undefined;\n const notifiedRunnerIds = new Set<string>();\n const pendingPromotedRunnerIds = new Set<string>();\n const pendingOperations = new Set<Promise<unknown>>();\n const trackOperation = <T>(operation: Promise<T>): Promise<T> => {\n pendingOperations.add(operation);\n void operation.then(\n () => pendingOperations.delete(operation),\n () => pendingOperations.delete(operation),\n );\n return operation;\n };\n const trackedBashRunService: IBashRunService = {\n run: (request) => trackOperation(bashRunService.run(request)),\n };\n const isCurrent = (generation: number, expectedSessionId = sessionId): boolean =>\n active && !disposed && generation === sessionGeneration && expectedSessionId === sessionId;\n const getTelemetry = (ctx: ExtensionContext): DoomTelemetry => {\n telemetry ??= createDoomTelemetry({\n serviceName: 'doom-runner',\n packageName: '@agimon-ai/doompi-runner',\n cwd: ctx.cwd,\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n return telemetry;\n };\n /** Performs one bounded pass over active state and explicitly monitored runners. */\n const refreshNow = async (shouldReconcile: boolean): Promise<void> => {\n const activeSessionId = sessionId;\n const generation = sessionGeneration;\n if (!activeSessionId || !isCurrent(generation, activeSessionId)) return;\n\n const activeRecords = await registry.list();\n if (!isCurrent(generation, activeSessionId)) return;\n let visibleActive = activeRecords;\n if (shouldReconcile) {\n const reconciled = await reconcileActiveRunners({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n startup: false,\n active: activeRecords,\n });\n for (const error of reconciled.errors) process.emitWarning(error);\n if (reconciled.reclaimed.length > 0) {\n const reclaimedIds = new Set(reconciled.reclaimed);\n visibleActive = activeRecords.filter((record) => !reclaimedIds.has(record.id));\n }\n }\n if (!isCurrent(generation, activeSessionId)) return;\n\n runners = visibleActive.filter((record) => record.sessionId === activeSessionId);\n const rootRunners = rootSessionId\n ? visibleActive.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId)\n : [];\n const runnerCount = rootRunners.length;\n if (lastRunnerCount !== runnerCount) {\n lastRunnerCount = runnerCount;\n footerContribution?.update(formatRunnerFooterContribution(runnerCount));\n if (sessionContext?.hasUI) sessionContext.ui.setStatus(RUNNER_STATUS_KEY, formatRunnerStatus(runnerCount));\n }\n\n const pendingIds = [...pendingPromotedRunnerIds];\n const monitored = await Promise.all(pendingIds.map((id) => registry.get(id, activeSessionId)));\n if (!isCurrent(generation, activeSessionId)) return;\n for (const [index, record] of monitored.entries()) {\n const id = pendingIds[index];\n if (!id) continue;\n if (!record || record.sessionId !== activeSessionId) {\n pendingPromotedRunnerIds.delete(id);\n continue;\n }\n if (record.state !== COMPLETED_STATE) continue;\n pendingPromotedRunnerIds.delete(record.id);\n if (notifiedRunnerIds.has(record.id)) continue;\n notifiedRunnerIds.add(record.id);\n const outcome = record.exit?.reason ?? COMPLETED_STATE;\n const code =\n record.exit?.code === null || record.exit?.code === undefined ? '' : `, exit code ${record.exit.code}`;\n const content = [\n `Background runner ${record.name} exited: ${outcome}${code}.`,\n `Runner ID: ${record.id}`,\n `Log: ${record.logPath}`,\n `Inspect: doom-runner logs ${record.id}`,\n ].join('\\n');\n pi.sendMessage(\n { customType: RUNNER_FINISHED_MESSAGE, content, display: true },\n { triggerTurn: true, deliverAs: 'steer' },\n );\n if (sessionContext) {\n void trackOperation(\n getTelemetry(sessionContext).recordEvent('doom_runner.process_finished', {\n outcome: 'completed',\n 'runner.exit_code': record.exit?.code ?? 0,\n 'runner.backend': record.backend,\n ...(Number.isFinite(Date.parse(record.startedAt))\n ? { duration_ms: Math.max(0, Date.now() - Date.parse(record.startedAt)) }\n : {}),\n }),\n );\n }\n }\n };\n\n /** Coalesces timer and event triggers without losing a requested reconciliation pass. */\n const refresh = (shouldReconcile = true): Promise<void> => {\n if (!sessionId || !sessionReady || disposed) return Promise.resolve();\n if (refreshInFlight) {\n refreshQueued = true;\n queuedReconciliation ||= shouldReconcile;\n return refreshInFlight;\n }\n\n const execution = (async () => {\n let reconcileNext = shouldReconcile;\n do {\n refreshQueued = false;\n queuedReconciliation = false;\n await refreshNow(reconcileNext);\n reconcileNext = queuedReconciliation;\n } while (refreshQueued && !disposed);\n })().finally(() => {\n if (refreshInFlight === execution) refreshInFlight = undefined;\n });\n refreshInFlight = execution;\n return execution;\n };\n\n const reportRefreshError = (error: unknown): void => {\n if (disposed) return;\n if (sessionContext) {\n void trackOperation(getTelemetry(sessionContext).recordError('doom_runner.refresh_failed', error));\n }\n process.emitWarning(`Could not refresh doom-runner: ${String(error)}`);\n };\n const scheduleRefresh = (shouldReconcile: boolean): void => {\n void refresh(shouldReconcile).catch(reportRefreshError);\n };\n const scheduleHistorySweep = (): void => {\n if (historySweepTimer || historySweepInFlight || disposed) return;\n historySweepTimer = setImmediate(() => {\n historySweepTimer = undefined;\n if (disposed) return;\n const execution = (async () => {\n const sweep = paths.sweepHistoryAsync\n ? await paths.sweepHistoryAsync(getLogTtlMs())\n : paths.sweepHistory(getLogTtlMs());\n for (const error of sweep.errors) process.emitWarning(error);\n })()\n .catch((error) => process.emitWarning(`Could not sweep doom-runner history: ${String(error)}`))\n .finally(() => {\n if (historySweepInFlight === execution) historySweepInFlight = undefined;\n });\n historySweepInFlight = execution;\n });\n historySweepTimer.unref?.();\n };\n\n const runCleanup = async (label: string, cleanup: () => void | Promise<void>): Promise<void> => {\n try {\n await cleanup();\n } catch (error) {\n process.emitWarning(`Doom-runner cleanup could not ${label}: ${String(error)}`);\n }\n };\n\n const shutdownRuntime = async (): Promise<void> => {\n if (disposed) return;\n active = false;\n disposed = true;\n sessionGeneration += 1;\n sessionReady = false;\n if (statusPoll) clearInterval(statusPoll);\n statusPoll = undefined;\n if (historySweepTimer) clearImmediate(historySweepTimer);\n historySweepTimer = undefined;\n const unsubscribe = unsubscribeRegistry;\n unsubscribeRegistry = undefined;\n if (unsubscribe) await runCleanup('unsubscribe from the runner registry', unsubscribe);\n\n await Promise.allSettled(pendingOperations);\n const activeRefresh = refreshInFlight;\n if (activeRefresh) await runCleanup('settle the runner refresh', () => activeRefresh);\n const activeHistorySweep = historySweepInFlight;\n if (activeHistorySweep) await runCleanup('settle runner history cleanup', () => activeHistorySweep);\n\n const disposePty = disposePtyProvider;\n disposePtyProvider = undefined;\n if (disposePty) await runCleanup('unregister the runner PTY provider', disposePty);\n await runCleanup('dispose runner PTYs', () => ptyHost.disposeAll());\n\n let owned: RunnerRecord[] = [];\n if (sessionId) {\n try {\n owned = await registry.listBySession(sessionId);\n } catch (error) {\n process.emitWarning(`Could not list runners during doom-runner shutdown: ${String(error)}`);\n }\n }\n for (const record of owned) {\n try {\n const stopped = await stopRunnerProcess(record, launcher, rmuxBackend);\n if (!stopped && processControl.isAlive(record.pid)) {\n process.emitWarning(`Could not stop runner ${record.id} during session shutdown`);\n continue;\n }\n await registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: stopped ? 'SIGTERM' : null,\n stopReason: 'session ended',\n },\n record.sessionId,\n );\n } catch (error) {\n process.emitWarning(`Could not clean up runner ${record.id}: ${String(error)}`);\n }\n }\n\n const footer = footerContribution;\n footerContribution = undefined;\n const leader = disposeLeader;\n disposeLeader = undefined;\n const context = sessionContext;\n const ownedTelemetry = telemetry;\n telemetry = undefined;\n sessionContext = undefined;\n sessionId = undefined;\n rootSessionId = undefined;\n runners = [];\n notifiedRunnerIds.clear();\n pendingPromotedRunnerIds.clear();\n\n await runCleanup('dispose the runner lifeline', () => lifeline.dispose());\n if (footer) await runCleanup('dispose the runner footer', () => footer.dispose());\n if (leader) await runCleanup('dispose the runner leader binding', leader);\n if (context?.hasUI) {\n await runCleanup('clear the runner status', () => context.ui.setStatus(RUNNER_STATUS_KEY, undefined));\n }\n await runCleanup('close the runner registry', () => registry.close());\n if (ownedTelemetry) {\n await runCleanup('record the runner session finish', () =>\n ownedTelemetry.recordEvent('doom_runner.session_finished', { outcome: 'stopped' }),\n );\n await runCleanup('stop runner telemetry', () => ownedTelemetry.shutdown());\n }\n };\n\n disposeRuntime = shutdownRuntime;\n\n footerContribution = registerDoomFooterContribution(pi, {\n source: LEADER_SOURCE,\n id: 'runner-count',\n order: RUNNER_FOOTER_ORDER,\n });\n unsubscribeRegistry = registry.subscribe(() => scheduleRefresh(false));\n statusPoll = setInterval(() => scheduleRefresh(true), RUNNER_STATUS_POLL_MS);\n statusPoll.unref?.();\n\n registerBashTool(pi, {\n bashRunService: trackedBashRunService,\n getSessionId: () => {\n if (!active || !sessionReady || !sessionId) throw new Error('doom-runner requires an active Pi session');\n return sessionId;\n },\n onRunnerStarted: (id) => {\n if (!active || !sessionReady) return;\n pendingPromotedRunnerIds.add(id);\n scheduleRefresh(false);\n },\n });\n\n registerRunnerCompactionRecovery(pi, {\n getSessionId: () => (active ? sessionId : undefined),\n listBySession: (activeSessionId) => (active ? registry.listBySession(activeSessionId) : Promise.resolve([])),\n });\n\n pi.registerCommand(COMMAND_NAME, {\n description: 'Open Runner Space: background processes started by bash',\n handler: async (_args, ctx) => {\n if (!active) return;\n const generation = sessionGeneration;\n const activeSessionId = sessionId;\n if (!ctx.hasUI) {\n ctx.ui.notify(ERR_REQUIRES_INTERACTIVE, 'error');\n return;\n }\n await refresh();\n if (!activeSessionId || !isCurrent(generation, activeSessionId)) return;\n await openRunnerSpace(ctx, {\n getRunners: () => (isCurrent(generation, activeSessionId) ? runners : []),\n getPtyRun: (name) =>\n isCurrent(generation, activeSessionId) ? (rmuxBackend.get(name) ?? ptyHost.get(name)) : undefined,\n readLog: (logPath) =>\n isCurrent(generation, activeSessionId) ? logReader.read(logPath, { lines: 1_000 }).text : '',\n stopRunner: async (id, reason) => {\n if (!isCurrent(generation, activeSessionId)) return;\n const record = runners.find((candidate) => candidate.id === id);\n if (!record) return;\n if (record.backend === RMUX_BACKEND && record.backendTarget) {\n await rmuxBackend.stop(record.backendTarget, record.pid);\n } else await launcher.stop(record.pid);\n if (!isCurrent(generation, activeSessionId)) return;\n await registry.complete(\n record.id,\n { reason: STOPPED_REASON, code: null, signal: null, stopReason: reason },\n record.sessionId,\n );\n if (isCurrent(generation, activeSessionId)) await refresh();\n },\n });\n },\n });\n\n disposeLeader = registerDoomLeaderContribution(pi, {\n source: LEADER_SOURCE,\n bindings: [\n {\n id: 'runners.open',\n path: [\n { key: 'r', label: 'runners', order: LEADER_GROUP_ORDER },\n { key: 'r', label: 'open', detail: 'background processes' },\n ],\n command: { name: COMMAND_NAME },\n },\n ],\n });\n\n pi.on(SESSION_START_EVENT, (_event, ctx) => {\n if (!active) return;\n const context = ctx as ExtensionContext;\n const activeSessionId = context.sessionManager.getSessionId();\n const previousSessionId = sessionId;\n const previousContext = sessionContext;\n const generation = ++sessionGeneration;\n sessionId = activeSessionId;\n rootSessionId = resolveRootSessionId(activeSessionId);\n sessionContext = context;\n sessionReady = false;\n lastRunnerCount = undefined;\n notifiedRunnerIds.clear();\n pendingPromotedRunnerIds.clear();\n\n const initializeSession = async (): Promise<void> => {\n if (!isCurrent(generation, activeSessionId)) return;\n if (previousSessionId && previousSessionId !== activeSessionId) {\n const previousPtyProvider = disposePtyProvider;\n disposePtyProvider = undefined;\n if (previousPtyProvider) {\n await runCleanup('unregister the previous session PTY provider', previousPtyProvider);\n }\n await runCleanup('dispose the previous session PTYs', () => ptyHost.disposeAll());\n await runCleanup('dispose the previous session lifeline', () => lifeline.dispose());\n if (previousContext?.hasUI) {\n await runCleanup('clear the previous session status', () =>\n previousContext.ui.setStatus(RUNNER_STATUS_KEY, undefined),\n );\n }\n const previousTelemetry = telemetry;\n telemetry = undefined;\n if (previousTelemetry) {\n await runCleanup('stop the previous session telemetry', () => previousTelemetry.shutdown());\n }\n if (!isCurrent(generation, activeSessionId)) return;\n }\n\n const activeTelemetry = getTelemetry(context);\n await activeTelemetry.recordEvent('doom_runner.session_started', { outcome: 'started' });\n if (!isCurrent(generation, activeSessionId)) return;\n paths.setSessionId(activeSessionId);\n // Awaited before anything can launch, so every runner this session starts\n // finds a lifeline it can connect to rather than a socket that is not\n // listening yet, which would read as an owner that is already gone.\n await lifeline.arm(activeSessionId);\n if (!isCurrent(generation, activeSessionId)) return;\n\n // This package owns the only pty in the workspace, so it lends one to any\n // extension that needs a command the user can watch and answer.\n disposePtyProvider?.();\n disposePtyProvider = provideRunnerPty(\n createProtocolRuntime({ bus: pi.events, source: LEADER_SOURCE, sessionId: activeSessionId }),\n {\n ptyHost,\n sessionId: () => (isCurrent(generation, activeSessionId) ? activeSessionId : undefined),\n onError: (error, id) => {\n if (!isCurrent(generation, activeSessionId)) return;\n void trackOperation(activeTelemetry.recordError('doom_runner.pty_launch_failed', error, { 'run.id': id }));\n },\n },\n );\n\n const retained = await registry.listAll(activeSessionId);\n if (!isCurrent(generation, activeSessionId)) return;\n for (const record of retained) {\n if (record.state === COMPLETED_STATE) notifiedRunnerIds.add(record.id);\n else if (record.promoted) pendingPromotedRunnerIds.add(record.id);\n }\n const legacy = await cleanupLegacyRunnerStore({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n paths,\n });\n if (!isCurrent(generation, activeSessionId)) return;\n const reconciled = await reconcileActiveRunners({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n startup: true,\n });\n if (!isCurrent(generation, activeSessionId)) return;\n sessionReady = true;\n await refresh(false);\n if (!isCurrent(generation, activeSessionId)) return;\n scheduleHistorySweep();\n const errors = [...legacy.errors, ...reconciled.errors];\n for (const error of errors) process.emitWarning(error);\n const reclaimedCount = legacy.reclaimed.length + reconciled.reclaimed.length;\n if (reclaimedCount > 0 && context.hasUI) {\n context.ui.notify(`Reclaimed ${reclaimedCount} stale runner record(s)`, 'warning');\n }\n await activeTelemetry.recordEvent('doom_runner.reconciled', {\n 'runner.reclaimed_count': reclaimedCount,\n 'runner.error_count': errors.length,\n outcome: errors.length === 0 ? 'completed' : 'degraded',\n });\n };\n\n const operation = lifecycleQueue.then(initializeSession, initializeSession);\n lifecycleQueue = operation.catch((error) => {\n process.emitWarning(`Could not initialize doom-runner session: ${String(error)}`);\n });\n return trackOperation(operation);\n });\n}\n\n/** The package's sole Pi factory; Pi reloads it and Cordis owns all package resources. */\nexport async function runnerExtension(pi: ExtensionAPI): Promise<void> {\n const cordis = new Context();\n let disposal: Promise<void> | undefined;\n const dispose = (): Promise<void> => {\n disposal ??= cordis.fiber.dispose();\n return disposal;\n };\n\n try {\n installRunnerRuntime(cordis, pi);\n pi.on(SESSION_SHUTDOWN_EVENT, dispose);\n } catch (error) {\n await dispose();\n throw error;\n }\n}\n\nexport default runnerExtension;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"extension.cjs","names":["createRunnerContainer","createDoomTelemetry","reconcileActiveRunners","formatRunnerFooterContribution","formatRunnerStatus","getLogTtlMs","stopRunnerProcess","DOOM_UI_HUB_SERVICE","requireDoomUiHub","openRunnerSpace","resolveRootSessionId","cleanupLegacyRunnerStore","readDoomReadinessCoordinator","createDoomReadinessCoordinator","connectDoomCordisHost"],"sources":["../../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport { connectDoomCordisHost } from '@agimon-ai/doompi-extension-contracts/cordis-host';\nimport {\n createDoomReadinessCoordinator,\n type DoomReadinessCoordinator,\n readDoomReadinessCoordinator,\n} from '@agimon-ai/doompi-extension-contracts/readiness';\nimport type { DoomFooterContributionHandle } from '@agimon-ai/doompi-extension-contracts/footer';\nimport { DOOM_UI_HUB_SERVICE, requireDoomUiHub } from '@agimon-ai/doompi-extension-contracts/ui-hub';\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { registerBashTool } from '../../commands/bash/bashTool.ts';\nimport { createRunnerContainer } from '../../container/index.ts';\nimport type { IBashRunService } from '../../types/bashRunService';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport { registerRunnerCompactionRecovery } from '../../services/runs/compaction.ts';\nimport { cleanupLegacyRunnerStore, reconcileActiveRunners, stopRunnerProcess } from '../../services/runs/reconcile.ts';\nimport { formatRunnerFooterContribution, formatRunnerStatus } from '../../tui/format.ts';\nimport { openRunnerSpace } from '../../tui/runnerSpace.ts';\nimport { getLogTtlMs } from '../../types/config.ts';\n\nconst LEADER_SOURCE = '@agimon-ai/doompi-runner';\n/** After doom-task's `t` (65) and before the core help group (70). */\nconst LEADER_GROUP_ORDER = 67;\nconst COMMAND_NAME = 'runners';\nconst ERR_REQUIRES_INTERACTIVE = '/runners requires interactive mode';\n\nconst SESSION_START_EVENT = 'session_start';\nconst RUNNER_FINISHED_MESSAGE = 'doom-runner-finished';\nconst COMPLETED_STATE = 'completed';\nconst RMUX_BACKEND = 'rmux';\nconst STOPPED_REASON = 'stopped';\nconst RUNNER_STATUS_KEY = 'doom-runner-runners';\nconst RUNNER_FOOTER_ORDER = 10;\nconst RUNNER_STATUS_POLL_MS = 500;\n\n/**\n * doom-runner: replaces pi's `bash` tool with a supervised one and provides a\n * CLI for anything it leaves running.\n */\nexport function installRunnerRuntime(cordis: Context, pi: ExtensionAPI): void {\n const container = createRunnerContainer();\n const registry = container.runnerRegistry;\n let disposeRuntime = async (): Promise<void> => {\n try {\n registry.close();\n } catch (error) {\n process.emitWarning(`Could not close a partially installed doom-runner registry: ${String(error)}`);\n }\n };\n cordis.effect(() => () => disposeRuntime(), `${LEADER_SOURCE}/runtime`);\n\n const launcher = container.launcher;\n const rmuxBackend = container.rmuxBackend;\n const logReader = container.logReader;\n const ptyHost = container.ptyHost;\n const bashRunService = container.bashRunService;\n const paths = container.paths;\n const processControl = container.processControl;\n const lifeline = container.lifeline;\n\n let active = true;\n let sessionGeneration = 0;\n let sessionId: string | undefined;\n let rootSessionId: string | undefined;\n let runners: RunnerRecord[] = [];\n let sessionContext: ExtensionContext | undefined;\n let footerContribution: DoomFooterContributionHandle | undefined;\n let telemetry: DoomTelemetry | undefined;\n let sessionReady = false;\n let disposed = false;\n let refreshInFlight: Promise<void> | undefined;\n let historySweepInFlight: Promise<void> | undefined;\n let historySweepTimer: ReturnType<typeof setImmediate> | undefined;\n let statusPoll: ReturnType<typeof setInterval> | undefined;\n let unsubscribeRegistry: (() => void) | undefined;\n let sessionInitialization: Promise<void> = Promise.resolve();\n let fallbackReadiness: DoomReadinessCoordinator | undefined;\n let refreshQueued = false;\n let queuedReconciliation = false;\n let lastRunnerCount: number | undefined;\n const notifiedRunnerIds = new Set<string>();\n const pendingPromotedRunnerIds = new Set<string>();\n const pendingOperations = new Set<Promise<unknown>>();\n const trackOperation = <T>(operation: Promise<T>): Promise<T> => {\n pendingOperations.add(operation);\n void operation.then(\n () => pendingOperations.delete(operation),\n () => pendingOperations.delete(operation),\n );\n return operation;\n };\n const waitForSessionReadiness = async (): Promise<void> => {\n await sessionInitialization;\n if (!active || !sessionReady || !sessionId) {\n throw new Error('doom-runner requires an active Pi session');\n }\n };\n const trackedBashRunService: IBashRunService = {\n run: async (request) => {\n await waitForSessionReadiness();\n return trackOperation(bashRunService.run(request));\n },\n };\n const isCurrent = (generation: number, expectedSessionId = sessionId): boolean =>\n active && !disposed && generation === sessionGeneration && expectedSessionId === sessionId;\n const getTelemetry = (ctx: ExtensionContext): DoomTelemetry => {\n telemetry ??= createDoomTelemetry({\n serviceName: 'doom-runner',\n packageName: '@agimon-ai/doompi-runner',\n cwd: ctx.cwd,\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n return telemetry;\n };\n /** Performs one bounded pass over active state and explicitly monitored runners. */\n const refreshNow = async (shouldReconcile: boolean): Promise<void> => {\n const activeSessionId = sessionId;\n const generation = sessionGeneration;\n if (!activeSessionId || !isCurrent(generation, activeSessionId)) return;\n\n const activeRecords = await registry.list();\n if (!isCurrent(generation, activeSessionId)) return;\n let visibleActive = activeRecords;\n if (shouldReconcile) {\n const reconciled = await reconcileActiveRunners({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n startup: false,\n active: activeRecords,\n });\n for (const error of reconciled.errors) process.emitWarning(error);\n if (reconciled.reclaimed.length > 0) {\n const reclaimedIds = new Set(reconciled.reclaimed);\n visibleActive = activeRecords.filter((record) => !reclaimedIds.has(record.id));\n }\n }\n if (!isCurrent(generation, activeSessionId)) return;\n\n runners = visibleActive.filter((record) => record.sessionId === activeSessionId);\n const rootRunners = rootSessionId\n ? visibleActive.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId)\n : [];\n const runnerCount = rootRunners.length;\n if (lastRunnerCount !== runnerCount) {\n lastRunnerCount = runnerCount;\n footerContribution?.update(formatRunnerFooterContribution(runnerCount));\n if (sessionContext?.hasUI) sessionContext.ui.setStatus(RUNNER_STATUS_KEY, formatRunnerStatus(runnerCount));\n }\n\n const pendingIds = [...pendingPromotedRunnerIds];\n const monitored = await Promise.all(pendingIds.map((id) => registry.get(id, activeSessionId)));\n if (!isCurrent(generation, activeSessionId)) return;\n for (const [index, record] of monitored.entries()) {\n const id = pendingIds[index];\n if (!id) continue;\n if (!record || record.sessionId !== activeSessionId) {\n pendingPromotedRunnerIds.delete(id);\n continue;\n }\n if (record.state !== COMPLETED_STATE) continue;\n pendingPromotedRunnerIds.delete(record.id);\n if (notifiedRunnerIds.has(record.id)) continue;\n notifiedRunnerIds.add(record.id);\n const outcome = record.exit?.reason ?? COMPLETED_STATE;\n const code =\n record.exit?.code === null || record.exit?.code === undefined ? '' : `, exit code ${record.exit.code}`;\n const content = [\n `Background runner ${record.name} exited: ${outcome}${code}.`,\n `Runner ID: ${record.id}`,\n `Log: ${record.logPath}`,\n `Inspect: doom-runner logs ${record.id}`,\n ].join('\\n');\n pi.sendMessage(\n { customType: RUNNER_FINISHED_MESSAGE, content, display: true },\n { triggerTurn: true, deliverAs: 'steer' },\n );\n if (sessionContext) {\n void trackOperation(\n getTelemetry(sessionContext).recordEvent('doom_runner.process_finished', {\n outcome: 'completed',\n 'runner.exit_code': record.exit?.code ?? 0,\n 'runner.backend': record.backend,\n ...(Number.isFinite(Date.parse(record.startedAt))\n ? { duration_ms: Math.max(0, Date.now() - Date.parse(record.startedAt)) }\n : {}),\n }),\n );\n }\n }\n };\n\n /** Coalesces timer and event triggers without losing a requested reconciliation pass. */\n const refresh = (shouldReconcile = true): Promise<void> => {\n if (!sessionId || !sessionReady || disposed) return Promise.resolve();\n if (refreshInFlight) {\n refreshQueued = true;\n queuedReconciliation ||= shouldReconcile;\n return refreshInFlight;\n }\n\n const execution = (async () => {\n let reconcileNext = shouldReconcile;\n do {\n refreshQueued = false;\n queuedReconciliation = false;\n await refreshNow(reconcileNext);\n reconcileNext = queuedReconciliation;\n } while (refreshQueued && !disposed);\n })().finally(() => {\n if (refreshInFlight === execution) refreshInFlight = undefined;\n });\n refreshInFlight = execution;\n return execution;\n };\n\n const reportRefreshError = (error: unknown): void => {\n if (disposed) return;\n if (sessionContext) {\n void trackOperation(getTelemetry(sessionContext).recordError('doom_runner.refresh_failed', error));\n }\n process.emitWarning(`Could not refresh doom-runner: ${String(error)}`);\n };\n const scheduleRefresh = (shouldReconcile: boolean): void => {\n void refresh(shouldReconcile).catch(reportRefreshError);\n };\n const scheduleHistorySweep = (): void => {\n if (historySweepTimer || historySweepInFlight || disposed) return;\n historySweepTimer = setImmediate(() => {\n historySweepTimer = undefined;\n if (disposed) return;\n const execution = (async () => {\n const sweep = paths.sweepHistoryAsync\n ? await paths.sweepHistoryAsync(getLogTtlMs())\n : paths.sweepHistory(getLogTtlMs());\n for (const error of sweep.errors) process.emitWarning(error);\n })()\n .catch((error) => process.emitWarning(`Could not sweep doom-runner history: ${String(error)}`))\n .finally(() => {\n if (historySweepInFlight === execution) historySweepInFlight = undefined;\n });\n historySweepInFlight = execution;\n });\n historySweepTimer.unref?.();\n };\n\n const runCleanup = async (label: string, cleanup: () => void | Promise<void>): Promise<void> => {\n try {\n await cleanup();\n } catch (error) {\n process.emitWarning(`Doom-runner cleanup could not ${label}: ${String(error)}`);\n }\n };\n\n const shutdownRuntime = async (): Promise<void> => {\n if (disposed) return;\n active = false;\n disposed = true;\n sessionGeneration += 1;\n sessionReady = false;\n if (statusPoll) clearInterval(statusPoll);\n statusPoll = undefined;\n if (historySweepTimer) clearImmediate(historySweepTimer);\n historySweepTimer = undefined;\n const unsubscribe = unsubscribeRegistry;\n unsubscribeRegistry = undefined;\n if (unsubscribe) await runCleanup('unsubscribe from the runner registry', unsubscribe);\n\n const ownedReadiness = fallbackReadiness;\n fallbackReadiness = undefined;\n if (ownedReadiness) await runCleanup('cancel standalone readiness work', () => ownedReadiness.dispose());\n await Promise.allSettled(pendingOperations);\n const activeRefresh = refreshInFlight;\n if (activeRefresh) await runCleanup('settle the runner refresh', () => activeRefresh);\n const activeHistorySweep = historySweepInFlight;\n if (activeHistorySweep) await runCleanup('settle runner history cleanup', () => activeHistorySweep);\n\n await runCleanup('dispose runner PTYs', () => ptyHost.disposeAll());\n\n let owned: RunnerRecord[] = [];\n if (sessionId) {\n try {\n owned = await registry.listBySession(sessionId);\n } catch (error) {\n process.emitWarning(`Could not list runners during doom-runner shutdown: ${String(error)}`);\n }\n }\n await Promise.all(\n owned.map(async (record) => {\n try {\n const stopped = await stopRunnerProcess(record, launcher, rmuxBackend);\n if (!stopped && processControl.isAlive(record.pid)) {\n process.emitWarning(`Could not stop runner ${record.id} during session shutdown`);\n return;\n }\n await registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: stopped ? 'SIGTERM' : null,\n stopReason: 'session ended',\n },\n record.sessionId,\n );\n } catch (error) {\n process.emitWarning(`Could not clean up runner ${record.id}: ${String(error)}`);\n }\n }),\n );\n\n const context = sessionContext;\n const ownedTelemetry = telemetry;\n telemetry = undefined;\n sessionContext = undefined;\n sessionId = undefined;\n rootSessionId = undefined;\n runners = [];\n notifiedRunnerIds.clear();\n pendingPromotedRunnerIds.clear();\n\n await Promise.all([\n runCleanup('dispose the runner lifeline', () => lifeline.dispose()),\n ...(context?.hasUI\n ? [runCleanup('clear the runner status', () => context.ui.setStatus(RUNNER_STATUS_KEY, undefined))]\n : []),\n ]);\n await runCleanup('close the runner registry', () => registry.close());\n if (ownedTelemetry) {\n await runCleanup('record the runner session finish', () =>\n ownedTelemetry.recordEvent('doom_runner.session_finished', { outcome: 'stopped' }),\n );\n await runCleanup('stop runner telemetry', () => ownedTelemetry.shutdown());\n }\n };\n\n disposeRuntime = shutdownRuntime;\n\n cordis.inject([DOOM_UI_HUB_SERVICE], (uiContext) => {\n const contribution = requireDoomUiHub(uiContext).registerFooter({\n source: LEADER_SOURCE,\n id: 'runner-count',\n order: RUNNER_FOOTER_ORDER,\n });\n footerContribution = contribution;\n return () => {\n contribution.dispose();\n if (footerContribution === contribution) footerContribution = undefined;\n };\n });\n unsubscribeRegistry = registry.subscribe(() => scheduleRefresh(false));\n statusPoll = setInterval(() => scheduleRefresh(true), RUNNER_STATUS_POLL_MS);\n statusPoll.unref?.();\n\n registerBashTool(pi, {\n bashRunService: trackedBashRunService,\n getSessionId: async () => {\n await waitForSessionReadiness();\n if (!sessionId) throw new Error('doom-runner requires an active Pi session');\n return sessionId;\n },\n onRunnerStarted: (id) => {\n if (!active || !sessionReady) return;\n pendingPromotedRunnerIds.add(id);\n scheduleRefresh(false);\n },\n });\n\n registerRunnerCompactionRecovery(pi, {\n getSessionId: async () => {\n try {\n await waitForSessionReadiness();\n return sessionId;\n } catch {\n return undefined;\n }\n },\n listBySession: async (activeSessionId) => {\n try {\n await waitForSessionReadiness();\n } catch {\n return [];\n }\n return active ? registry.listBySession(activeSessionId) : [];\n },\n });\n\n pi.registerCommand(COMMAND_NAME, {\n description: 'Open Runner Space: background processes started by bash',\n handler: async (_args, ctx) => {\n if (!active) return;\n const generation = sessionGeneration;\n const activeSessionId = sessionId;\n if (!activeSessionId) return;\n if (!ctx.hasUI) {\n ctx.ui.notify(ERR_REQUIRES_INTERACTIVE, 'error');\n return;\n }\n await waitForSessionReadiness();\n await refresh();\n if (!activeSessionId || !isCurrent(generation, activeSessionId)) return;\n await openRunnerSpace(ctx, {\n getRunners: () => (isCurrent(generation, activeSessionId) ? runners : []),\n getPtyRun: (name) =>\n isCurrent(generation, activeSessionId) ? (rmuxBackend.get(name) ?? ptyHost.get(name)) : undefined,\n readLog: (logPath) =>\n isCurrent(generation, activeSessionId) ? logReader.read(logPath, { lines: 1_000 }).text : '',\n stopRunner: async (id, reason) => {\n if (!isCurrent(generation, activeSessionId)) return;\n const record = runners.find((candidate) => candidate.id === id);\n if (!record) return;\n if (record.backend === RMUX_BACKEND && record.backendTarget) {\n await rmuxBackend.stop(record.backendTarget, record.pid);\n } else await launcher.stop(record.pid);\n if (!isCurrent(generation, activeSessionId)) return;\n await registry.complete(\n record.id,\n { reason: STOPPED_REASON, code: null, signal: null, stopReason: reason },\n record.sessionId,\n );\n if (isCurrent(generation, activeSessionId)) await refresh();\n },\n });\n },\n });\n\n cordis.inject([DOOM_UI_HUB_SERVICE], (uiContext) => {\n const contribution = requireDoomUiHub(uiContext).registerLeader({\n source: LEADER_SOURCE,\n bindings: [\n {\n id: 'runners.open',\n path: [\n { key: 'r', label: 'runners', order: LEADER_GROUP_ORDER },\n { key: 'r', label: 'open', detail: 'background processes' },\n ],\n command: { name: COMMAND_NAME },\n },\n ],\n });\n return () => contribution.dispose();\n });\n\n pi.on(SESSION_START_EVENT, (_event, ctx) => {\n if (!active) return;\n const context = ctx as ExtensionContext;\n const activeSessionId = context.sessionManager.getSessionId();\n const previousSessionId = sessionId;\n const previousContext = sessionContext;\n const generation = ++sessionGeneration;\n sessionId = activeSessionId;\n rootSessionId = resolveRootSessionId(activeSessionId);\n sessionContext = context;\n sessionReady = false;\n lastRunnerCount = undefined;\n notifiedRunnerIds.clear();\n pendingPromotedRunnerIds.clear();\n\n const initializeSession = async (signal: AbortSignal): Promise<readonly string[]> => {\n const stillCurrent = (): boolean => !signal.aborted && isCurrent(generation, activeSessionId);\n if (!stillCurrent()) return [];\n if (previousSessionId && previousSessionId !== activeSessionId) {\n await runCleanup('dispose the previous session PTYs', () => ptyHost.disposeAll());\n await runCleanup('dispose the previous session lifeline', () => lifeline.dispose());\n if (previousContext?.hasUI) {\n await runCleanup('clear the previous session status', () =>\n previousContext.ui.setStatus(RUNNER_STATUS_KEY, undefined),\n );\n }\n const previousTelemetry = telemetry;\n telemetry = undefined;\n if (previousTelemetry) {\n await runCleanup('stop the previous session telemetry', () => previousTelemetry.shutdown());\n }\n if (!stillCurrent()) return [];\n }\n\n const activeTelemetry = getTelemetry(context);\n await activeTelemetry.recordEvent('doom_runner.session_started', { outcome: 'started' });\n if (!stillCurrent()) return [];\n paths.setSessionId(activeSessionId);\n // Awaited before anything can launch, so every runner this session starts\n // finds a lifeline it can connect to rather than a socket that is not\n // listening yet, which would read as an owner that is already gone.\n await lifeline.arm(activeSessionId);\n if (!stillCurrent()) return [];\n\n const retained = await registry.listAll(activeSessionId);\n if (!stillCurrent()) return [];\n for (const record of retained) {\n if (record.state === COMPLETED_STATE) notifiedRunnerIds.add(record.id);\n else if (record.promoted) pendingPromotedRunnerIds.add(record.id);\n }\n const legacy = await cleanupLegacyRunnerStore({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n paths,\n });\n if (!stillCurrent()) return [];\n const reconciled = await reconcileActiveRunners({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n startup: true,\n });\n if (!stillCurrent()) return [];\n sessionReady = true;\n await refresh(false);\n if (!stillCurrent()) return [];\n scheduleHistorySweep();\n const errors = [...legacy.errors, ...reconciled.errors];\n const reclaimedCount = legacy.reclaimed.length + reconciled.reclaimed.length;\n if (reclaimedCount > 0 && context.hasUI) {\n context.ui.notify(`Reclaimed ${reclaimedCount} stale runner record(s)`, 'warning');\n }\n await activeTelemetry.recordEvent('doom_runner.reconciled', {\n 'runner.reclaimed_count': reclaimedCount,\n 'runner.error_count': errors.length,\n outcome: errors.length === 0 ? 'completed' : 'degraded',\n });\n return errors;\n };\n\n const previousInitialization = sessionInitialization.catch(() => undefined);\n const startReadiness = async (): Promise<void> => {\n await previousInitialization;\n if (!isCurrent(generation, activeSessionId)) return;\n const coordinator =\n readDoomReadinessCoordinator(cordis) ??\n (fallbackReadiness ??= createDoomReadinessCoordinator({\n notify: (notification) => {\n process.emitWarning(\n `${notification.packageId} initialization ${notification.state}: ${notification.error?.message ?? notification.diagnostics.join('; ')}`,\n );\n },\n }));\n const handle = coordinator.start(LEADER_SOURCE, `${activeSessionId}:${generation}`, async (signal) => ({\n value: undefined,\n diagnostics: await initializeSession(signal),\n }));\n await handle.wait();\n };\n const operation = startReadiness();\n sessionInitialization = operation;\n // The coordinator reports a failed generation once; this branch only marks\n // the detached waiter handled so Pi is not held open by the notification path.\n void trackOperation(operation).catch(() => undefined);\n });\n}\n\n/** The package's sole Pi factory; Pi reloads it and Cordis owns all package resources. */\nexport async function runnerExtension(pi: ExtensionAPI): Promise<void> {\n const connection = await connectDoomCordisHost(pi, LEADER_SOURCE);\n const fiber = connection.root.plugin(runnerPlugin, { pi });\n try {\n await fiber;\n } catch (error) {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n throw error;\n }\n let disposal: Promise<void> | undefined;\n pi.on(\n 'session_shutdown',\n () =>\n (disposal ??= (async () => {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n })()),\n );\n}\n\ninterface RunnerPluginConfig {\n readonly pi: ExtensionAPI;\n}\n\nfunction runnerPlugin(cordis: Context, config: RunnerPluginConfig): void {\n installRunnerRuntime(cordis, config.pi);\n}\n\nexport default runnerExtension;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension.d.cts","names":[],"sources":["../../../../src/adapters/pi/extension.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"extension.d.cts","names":[],"sources":["../../../../src/adapters/pi/extension.ts"],"mappings":";;;;iBAkjBsB,gBAAgB,IAAI,eAAe"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Context } from "@deepseek-ai/cordis";
|
|
2
1
|
import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import "@deepseek-ai/cordis";
|
|
3
3
|
//#region src/adapters/pi/extension.d.ts
|
|
4
4
|
/** The package's sole Pi factory; Pi reloads it and Cordis owns all package resources. */
|
|
5
5
|
declare function runnerExtension(pi: ExtensionAPI): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension.d.mts","names":[],"sources":["../../../../src/adapters/pi/extension.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"extension.d.mts","names":[],"sources":["../../../../src/adapters/pi/extension.ts"],"mappings":";;;;iBAkjBsB,gBAAgB,IAAI,eAAe"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{registerRunnerCompactionRecovery as e}from"../../services/runs/compaction.mjs";import{getLogTtlMs as t}from"../../types/config.mjs";import{createRunnerContainer as n}from"../../container/index.mjs";import{
|
|
2
|
-
`);_.sendMessage({customType:`doom-runner-finished`,content:o,display:!0},{triggerTurn:!0,deliverAs:`steer`}),
|
|
1
|
+
import{registerRunnerCompactionRecovery as e}from"../../services/runs/compaction.mjs";import{getLogTtlMs as t}from"../../types/config.mjs";import{createRunnerContainer as n}from"../../container/index.mjs";import{cleanupLegacyRunnerStore as r,reconcileActiveRunners as i,stopRunnerProcess as a}from"../../services/runs/reconcile.mjs";import{registerBashTool as o}from"../../commands/bash/bashTool.mjs";import{formatRunnerFooterContribution as s,formatRunnerStatus as c}from"../../tui/format.mjs";import{openRunnerSpace as l}from"../../tui/runnerSpace.mjs";import{resolveRootSessionId as u}from"@agimon-ai/doompi-extension-contracts/child-process";import{connectDoomCordisHost as d}from"@agimon-ai/doompi-extension-contracts/cordis-host";import{createDoomReadinessCoordinator as ee,readDoomReadinessCoordinator as f}from"@agimon-ai/doompi-extension-contracts/readiness";import{DOOM_UI_HUB_SERVICE as p,requireDoomUiHub as m}from"@agimon-ai/doompi-extension-contracts/ui-hub";import{createDoomTelemetry as te}from"@agimon-ai/doompi-telemetry";const h=`@agimon-ai/doompi-runner`,ne=`runners`,re=`completed`,ie=`stopped`,g=`doom-runner-runners`;function _(d,_){let v=n(),y=v.runnerRegistry,ae=async()=>{try{y.close()}catch(e){process.emitWarning(`Could not close a partially installed doom-runner registry: ${String(e)}`)}};d.effect(()=>()=>ae(),`${h}/runtime`);let b=v.launcher,x=v.rmuxBackend,oe=v.logReader,S=v.ptyHost,se=v.bashRunService,C=v.paths,w=v.processControl,ce=v.lifeline,T=!0,E=0,D,O,k=[],A,j,M,N=!1,P=!1,F,I,L,R,z,B=Promise.resolve(),V,H=!1,U=!1,W,G=new Set,K=new Set,q=new Set,J=e=>(q.add(e),e.then(()=>q.delete(e),()=>q.delete(e)),e),Y=async()=>{if(await B,!T||!N||!D)throw Error(`doom-runner requires an active Pi session`)},le={run:async e=>(await Y(),J(se.run(e)))},X=(e,t=D)=>T&&!P&&e===E&&t===D,Z=e=>(M??=te({serviceName:`doom-runner`,packageName:`@agimon-ai/doompi-runner`,cwd:e.cwd,env:process.env,enableLogs:!0,enableTraces:!0}),M),ue=async e=>{let t=D,n=E;if(!t||!X(n,t))return;let r=await y.list();if(!X(n,t))return;let a=r;if(e){let e=await i({registry:y,launcher:b,rmuxBackend:x,processControl:w,currentHostPid:process.pid,startup:!1,active:r});for(let t of e.errors)process.emitWarning(t);if(e.reclaimed.length>0){let t=new Set(e.reclaimed);a=r.filter(e=>!t.has(e.id))}}if(!X(n,t))return;k=a.filter(e=>e.sessionId===t);let o=(O?a.filter(e=>(e.rootSessionId??e.sessionId)===O):[]).length;W!==o&&(W=o,j?.update(s(o)),A?.hasUI&&A.ui.setStatus(g,c(o)));let l=[...K],u=await Promise.all(l.map(e=>y.get(e,t)));if(X(n,t))for(let[e,n]of u.entries()){let r=l[e];if(!r)continue;if(!n||n.sessionId!==t){K.delete(r);continue}if(n.state!==re||(K.delete(n.id),G.has(n.id)))continue;G.add(n.id);let i=n.exit?.reason??re,a=n.exit?.code===null||n.exit?.code===void 0?``:`, exit code ${n.exit.code}`,o=[`Background runner ${n.name} exited: ${i}${a}.`,`Runner ID: ${n.id}`,`Log: ${n.logPath}`,`Inspect: doom-runner logs ${n.id}`].join(`
|
|
2
|
+
`);_.sendMessage({customType:`doom-runner-finished`,content:o,display:!0},{triggerTurn:!0,deliverAs:`steer`}),A&&J(Z(A).recordEvent(`doom_runner.process_finished`,{outcome:`completed`,"runner.exit_code":n.exit?.code??0,"runner.backend":n.backend,...Number.isFinite(Date.parse(n.startedAt))?{duration_ms:Math.max(0,Date.now()-Date.parse(n.startedAt))}:{}}))}},Q=(e=!0)=>{if(!D||!N||P)return Promise.resolve();if(F)return H=!0,U||=e,F;let t=(async()=>{let t=e;do H=!1,U=!1,await ue(t),t=U;while(H&&!P)})().finally(()=>{F===t&&(F=void 0)});return F=t,t},de=e=>{P||(A&&J(Z(A).recordError(`doom_runner.refresh_failed`,e)),process.emitWarning(`Could not refresh doom-runner: ${String(e)}`))},fe=e=>{Q(e).catch(de)},pe=()=>{L||I||P||(L=setImmediate(()=>{if(L=void 0,P)return;let e=(async()=>{let e=C.sweepHistoryAsync?await C.sweepHistoryAsync(t()):C.sweepHistory(t());for(let t of e.errors)process.emitWarning(t)})().catch(e=>process.emitWarning(`Could not sweep doom-runner history: ${String(e)}`)).finally(()=>{I===e&&(I=void 0)});I=e}),L.unref?.())},$=async(e,t)=>{try{await t()}catch(t){process.emitWarning(`Doom-runner cleanup could not ${e}: ${String(t)}`)}};ae=async()=>{if(P)return;T=!1,P=!0,E+=1,N=!1,R&&clearInterval(R),R=void 0,L&&clearImmediate(L),L=void 0;let e=z;z=void 0,e&&await $(`unsubscribe from the runner registry`,e);let t=V;V=void 0,t&&await $(`cancel standalone readiness work`,()=>t.dispose()),await Promise.allSettled(q);let n=F;n&&await $(`settle the runner refresh`,()=>n);let r=I;r&&await $(`settle runner history cleanup`,()=>r),await $(`dispose runner PTYs`,()=>S.disposeAll());let i=[];if(D)try{i=await y.listBySession(D)}catch(e){process.emitWarning(`Could not list runners during doom-runner shutdown: ${String(e)}`)}await Promise.all(i.map(async e=>{try{let t=await a(e,b,x);if(!t&&w.isAlive(e.pid)){process.emitWarning(`Could not stop runner ${e.id} during session shutdown`);return}await y.complete(e.id,{reason:ie,code:null,signal:t?`SIGTERM`:null,stopReason:`session ended`},e.sessionId)}catch(t){process.emitWarning(`Could not clean up runner ${e.id}: ${String(t)}`)}}));let o=A,s=M;M=void 0,A=void 0,D=void 0,O=void 0,k=[],G.clear(),K.clear(),await Promise.all([$(`dispose the runner lifeline`,()=>ce.dispose()),...o?.hasUI?[$(`clear the runner status`,()=>o.ui.setStatus(g,void 0))]:[]]),await $(`close the runner registry`,()=>y.close()),s&&(await $(`record the runner session finish`,()=>s.recordEvent(`doom_runner.session_finished`,{outcome:`stopped`})),await $(`stop runner telemetry`,()=>s.shutdown()))},d.inject([p],e=>{let t=m(e).registerFooter({source:h,id:`runner-count`,order:10});return j=t,()=>{t.dispose(),j===t&&(j=void 0)}}),z=y.subscribe(()=>fe(!1)),R=setInterval(()=>fe(!0),500),R.unref?.(),o(_,{bashRunService:le,getSessionId:async()=>{if(await Y(),!D)throw Error(`doom-runner requires an active Pi session`);return D},onRunnerStarted:e=>{!T||!N||(K.add(e),fe(!1))}}),e(_,{getSessionId:async()=>{try{return await Y(),D}catch{return}},listBySession:async e=>{try{await Y()}catch{return[]}return T?y.listBySession(e):[]}}),_.registerCommand(ne,{description:`Open Runner Space: background processes started by bash`,handler:async(e,t)=>{if(!T)return;let n=E,r=D;if(r){if(!t.hasUI){t.ui.notify(`/runners requires interactive mode`,`error`);return}await Y(),await Q(),!(!r||!X(n,r))&&await l(t,{getRunners:()=>X(n,r)?k:[],getPtyRun:e=>X(n,r)?x.get(e)??S.get(e):void 0,readLog:e=>X(n,r)?oe.read(e,{lines:1e3}).text:``,stopRunner:async(e,t)=>{if(!X(n,r))return;let i=k.find(t=>t.id===e);i&&(i.backend===`rmux`&&i.backendTarget?await x.stop(i.backendTarget,i.pid):await b.stop(i.pid),X(n,r)&&(await y.complete(i.id,{reason:ie,code:null,signal:null,stopReason:t},i.sessionId),X(n,r)&&await Q()))}})}}}),d.inject([p],e=>{let t=m(e).registerLeader({source:h,bindings:[{id:`runners.open`,path:[{key:`r`,label:`runners`,order:67},{key:`r`,label:`open`,detail:`background processes`}],command:{name:ne}}]});return()=>t.dispose()}),_.on(`session_start`,(e,t)=>{if(!T)return;let n=t,a=n.sessionManager.getSessionId(),o=D,s=A,c=++E;D=a,O=u(a),A=n,N=!1,W=void 0,G.clear(),K.clear();let l=async e=>{let t=()=>!e.aborted&&X(c,a);if(!t())return[];if(o&&o!==a){await $(`dispose the previous session PTYs`,()=>S.disposeAll()),await $(`dispose the previous session lifeline`,()=>ce.dispose()),s?.hasUI&&await $(`clear the previous session status`,()=>s.ui.setStatus(g,void 0));let e=M;if(M=void 0,e&&await $(`stop the previous session telemetry`,()=>e.shutdown()),!t())return[]}let l=Z(n);if(await l.recordEvent(`doom_runner.session_started`,{outcome:`started`}),!t()||(C.setSessionId(a),await ce.arm(a),!t()))return[];let u=await y.listAll(a);if(!t())return[];for(let e of u)e.state===re?G.add(e.id):e.promoted&&K.add(e.id);let d=await r({registry:y,launcher:b,rmuxBackend:x,processControl:w,currentHostPid:process.pid,paths:C});if(!t())return[];let ee=await i({registry:y,launcher:b,rmuxBackend:x,processControl:w,currentHostPid:process.pid,startup:!0});if(!t()||(N=!0,await Q(!1),!t()))return[];pe();let f=[...d.errors,...ee.errors],p=d.reclaimed.length+ee.reclaimed.length;return p>0&&n.hasUI&&n.ui.notify(`Reclaimed ${p} stale runner record(s)`,`warning`),await l.recordEvent(`doom_runner.reconciled`,{"runner.reclaimed_count":p,"runner.error_count":f.length,outcome:f.length===0?`completed`:`degraded`}),f},p=B.catch(()=>void 0),m=(async()=>{await p,X(c,a)&&await(f(d)??(V??=ee({notify:e=>{process.emitWarning(`${e.packageId} initialization ${e.state}: ${e.error?.message??e.diagnostics.join(`; `)}`)}}))).start(h,`${a}:${c}`,async e=>({value:void 0,diagnostics:await l(e)})).wait()})();B=m,J(m).catch(()=>void 0)})}async function v(e){let t=await d(e,h),n=t.root.plugin(y,{pi:e});try{await n}catch(e){try{await n.dispose()}finally{await t.dispose()}throw e}let r;e.on(`session_shutdown`,()=>r??=(async()=>{try{await n.dispose()}finally{await t.dispose()}})())}function y(e,t){_(e,t.pi)}export{v as default,v as runnerExtension,_ as installRunnerRuntime};
|
|
3
3
|
//# sourceMappingURL=extension.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension.mjs","names":[],"sources":["../../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport { registerDoomFooterContribution } from '@agimon-ai/doompi-ui/footer';\nimport { registerDoomLeaderContribution } from '@agimon-ai/doompi-ui/leader';\nimport { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { registerBashTool } from '../../commands/bash/bashTool.ts';\nimport { createRunnerContainer } from '../../container/index.ts';\nimport type { IBashRunService } from '../../types/bashRunService';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport { registerRunnerCompactionRecovery } from '../../services/runs/compaction.ts';\nimport { cleanupLegacyRunnerStore, reconcileActiveRunners, stopRunnerProcess } from '../../services/runs/reconcile.ts';\nimport { formatRunnerFooterContribution, formatRunnerStatus } from '../../tui/format.ts';\nimport { openRunnerSpace } from '../../tui/runnerSpace.ts';\nimport { getLogTtlMs } from '../../types/config.ts';\nimport { provideRunnerPty } from '../pty/ptyProvider.ts';\n\nconst LEADER_SOURCE = '@agimon-ai/doompi-runner';\n/** After doom-task's `t` (65) and before the core help group (70). */\nconst LEADER_GROUP_ORDER = 67;\nconst COMMAND_NAME = 'runners';\nconst ERR_REQUIRES_INTERACTIVE = '/runners requires interactive mode';\n\nconst SESSION_START_EVENT = 'session_start';\nconst SESSION_SHUTDOWN_EVENT = 'session_shutdown';\nconst RUNNER_FINISHED_MESSAGE = 'doom-runner-finished';\nconst COMPLETED_STATE = 'completed';\nconst RMUX_BACKEND = 'rmux';\nconst STOPPED_REASON = 'stopped';\nconst RUNNER_STATUS_KEY = 'doom-runner-runners';\nconst RUNNER_FOOTER_ORDER = 10;\nconst RUNNER_STATUS_POLL_MS = 500;\n\n/**\n * doom-runner: replaces pi's `bash` tool with a supervised one and provides a\n * CLI for anything it leaves running.\n */\nexport function installRunnerRuntime(cordis: Context, pi: ExtensionAPI): void {\n const container = createRunnerContainer();\n const registry = container.runnerRegistry;\n let disposeRuntime = async (): Promise<void> => {\n try {\n registry.close();\n } catch (error) {\n process.emitWarning(`Could not close a partially installed doom-runner registry: ${String(error)}`);\n }\n };\n cordis.effect(() => () => disposeRuntime(), `${LEADER_SOURCE}/runtime`);\n\n const launcher = container.launcher;\n const rmuxBackend = container.rmuxBackend;\n const logReader = container.logReader;\n const ptyHost = container.ptyHost;\n const bashRunService = container.bashRunService;\n const paths = container.paths;\n const processControl = container.processControl;\n const lifeline = container.lifeline;\n\n let active = true;\n let sessionGeneration = 0;\n let sessionId: string | undefined;\n let rootSessionId: string | undefined;\n let runners: RunnerRecord[] = [];\n let sessionContext: ExtensionContext | undefined;\n let disposePtyProvider: (() => void) | undefined;\n let disposeLeader: (() => void) | undefined;\n let footerContribution: ReturnType<typeof registerDoomFooterContribution> | undefined;\n let telemetry: DoomTelemetry | undefined;\n let sessionReady = false;\n let disposed = false;\n let refreshInFlight: Promise<void> | undefined;\n let historySweepInFlight: Promise<void> | undefined;\n let historySweepTimer: ReturnType<typeof setImmediate> | undefined;\n let statusPoll: ReturnType<typeof setInterval> | undefined;\n let unsubscribeRegistry: (() => void) | undefined;\n let lifecycleQueue: Promise<void> = Promise.resolve();\n let refreshQueued = false;\n let queuedReconciliation = false;\n let lastRunnerCount: number | undefined;\n const notifiedRunnerIds = new Set<string>();\n const pendingPromotedRunnerIds = new Set<string>();\n const pendingOperations = new Set<Promise<unknown>>();\n const trackOperation = <T>(operation: Promise<T>): Promise<T> => {\n pendingOperations.add(operation);\n void operation.then(\n () => pendingOperations.delete(operation),\n () => pendingOperations.delete(operation),\n );\n return operation;\n };\n const trackedBashRunService: IBashRunService = {\n run: (request) => trackOperation(bashRunService.run(request)),\n };\n const isCurrent = (generation: number, expectedSessionId = sessionId): boolean =>\n active && !disposed && generation === sessionGeneration && expectedSessionId === sessionId;\n const getTelemetry = (ctx: ExtensionContext): DoomTelemetry => {\n telemetry ??= createDoomTelemetry({\n serviceName: 'doom-runner',\n packageName: '@agimon-ai/doompi-runner',\n cwd: ctx.cwd,\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n return telemetry;\n };\n /** Performs one bounded pass over active state and explicitly monitored runners. */\n const refreshNow = async (shouldReconcile: boolean): Promise<void> => {\n const activeSessionId = sessionId;\n const generation = sessionGeneration;\n if (!activeSessionId || !isCurrent(generation, activeSessionId)) return;\n\n const activeRecords = await registry.list();\n if (!isCurrent(generation, activeSessionId)) return;\n let visibleActive = activeRecords;\n if (shouldReconcile) {\n const reconciled = await reconcileActiveRunners({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n startup: false,\n active: activeRecords,\n });\n for (const error of reconciled.errors) process.emitWarning(error);\n if (reconciled.reclaimed.length > 0) {\n const reclaimedIds = new Set(reconciled.reclaimed);\n visibleActive = activeRecords.filter((record) => !reclaimedIds.has(record.id));\n }\n }\n if (!isCurrent(generation, activeSessionId)) return;\n\n runners = visibleActive.filter((record) => record.sessionId === activeSessionId);\n const rootRunners = rootSessionId\n ? visibleActive.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId)\n : [];\n const runnerCount = rootRunners.length;\n if (lastRunnerCount !== runnerCount) {\n lastRunnerCount = runnerCount;\n footerContribution?.update(formatRunnerFooterContribution(runnerCount));\n if (sessionContext?.hasUI) sessionContext.ui.setStatus(RUNNER_STATUS_KEY, formatRunnerStatus(runnerCount));\n }\n\n const pendingIds = [...pendingPromotedRunnerIds];\n const monitored = await Promise.all(pendingIds.map((id) => registry.get(id, activeSessionId)));\n if (!isCurrent(generation, activeSessionId)) return;\n for (const [index, record] of monitored.entries()) {\n const id = pendingIds[index];\n if (!id) continue;\n if (!record || record.sessionId !== activeSessionId) {\n pendingPromotedRunnerIds.delete(id);\n continue;\n }\n if (record.state !== COMPLETED_STATE) continue;\n pendingPromotedRunnerIds.delete(record.id);\n if (notifiedRunnerIds.has(record.id)) continue;\n notifiedRunnerIds.add(record.id);\n const outcome = record.exit?.reason ?? COMPLETED_STATE;\n const code =\n record.exit?.code === null || record.exit?.code === undefined ? '' : `, exit code ${record.exit.code}`;\n const content = [\n `Background runner ${record.name} exited: ${outcome}${code}.`,\n `Runner ID: ${record.id}`,\n `Log: ${record.logPath}`,\n `Inspect: doom-runner logs ${record.id}`,\n ].join('\\n');\n pi.sendMessage(\n { customType: RUNNER_FINISHED_MESSAGE, content, display: true },\n { triggerTurn: true, deliverAs: 'steer' },\n );\n if (sessionContext) {\n void trackOperation(\n getTelemetry(sessionContext).recordEvent('doom_runner.process_finished', {\n outcome: 'completed',\n 'runner.exit_code': record.exit?.code ?? 0,\n 'runner.backend': record.backend,\n ...(Number.isFinite(Date.parse(record.startedAt))\n ? { duration_ms: Math.max(0, Date.now() - Date.parse(record.startedAt)) }\n : {}),\n }),\n );\n }\n }\n };\n\n /** Coalesces timer and event triggers without losing a requested reconciliation pass. */\n const refresh = (shouldReconcile = true): Promise<void> => {\n if (!sessionId || !sessionReady || disposed) return Promise.resolve();\n if (refreshInFlight) {\n refreshQueued = true;\n queuedReconciliation ||= shouldReconcile;\n return refreshInFlight;\n }\n\n const execution = (async () => {\n let reconcileNext = shouldReconcile;\n do {\n refreshQueued = false;\n queuedReconciliation = false;\n await refreshNow(reconcileNext);\n reconcileNext = queuedReconciliation;\n } while (refreshQueued && !disposed);\n })().finally(() => {\n if (refreshInFlight === execution) refreshInFlight = undefined;\n });\n refreshInFlight = execution;\n return execution;\n };\n\n const reportRefreshError = (error: unknown): void => {\n if (disposed) return;\n if (sessionContext) {\n void trackOperation(getTelemetry(sessionContext).recordError('doom_runner.refresh_failed', error));\n }\n process.emitWarning(`Could not refresh doom-runner: ${String(error)}`);\n };\n const scheduleRefresh = (shouldReconcile: boolean): void => {\n void refresh(shouldReconcile).catch(reportRefreshError);\n };\n const scheduleHistorySweep = (): void => {\n if (historySweepTimer || historySweepInFlight || disposed) return;\n historySweepTimer = setImmediate(() => {\n historySweepTimer = undefined;\n if (disposed) return;\n const execution = (async () => {\n const sweep = paths.sweepHistoryAsync\n ? await paths.sweepHistoryAsync(getLogTtlMs())\n : paths.sweepHistory(getLogTtlMs());\n for (const error of sweep.errors) process.emitWarning(error);\n })()\n .catch((error) => process.emitWarning(`Could not sweep doom-runner history: ${String(error)}`))\n .finally(() => {\n if (historySweepInFlight === execution) historySweepInFlight = undefined;\n });\n historySweepInFlight = execution;\n });\n historySweepTimer.unref?.();\n };\n\n const runCleanup = async (label: string, cleanup: () => void | Promise<void>): Promise<void> => {\n try {\n await cleanup();\n } catch (error) {\n process.emitWarning(`Doom-runner cleanup could not ${label}: ${String(error)}`);\n }\n };\n\n const shutdownRuntime = async (): Promise<void> => {\n if (disposed) return;\n active = false;\n disposed = true;\n sessionGeneration += 1;\n sessionReady = false;\n if (statusPoll) clearInterval(statusPoll);\n statusPoll = undefined;\n if (historySweepTimer) clearImmediate(historySweepTimer);\n historySweepTimer = undefined;\n const unsubscribe = unsubscribeRegistry;\n unsubscribeRegistry = undefined;\n if (unsubscribe) await runCleanup('unsubscribe from the runner registry', unsubscribe);\n\n await Promise.allSettled(pendingOperations);\n const activeRefresh = refreshInFlight;\n if (activeRefresh) await runCleanup('settle the runner refresh', () => activeRefresh);\n const activeHistorySweep = historySweepInFlight;\n if (activeHistorySweep) await runCleanup('settle runner history cleanup', () => activeHistorySweep);\n\n const disposePty = disposePtyProvider;\n disposePtyProvider = undefined;\n if (disposePty) await runCleanup('unregister the runner PTY provider', disposePty);\n await runCleanup('dispose runner PTYs', () => ptyHost.disposeAll());\n\n let owned: RunnerRecord[] = [];\n if (sessionId) {\n try {\n owned = await registry.listBySession(sessionId);\n } catch (error) {\n process.emitWarning(`Could not list runners during doom-runner shutdown: ${String(error)}`);\n }\n }\n for (const record of owned) {\n try {\n const stopped = await stopRunnerProcess(record, launcher, rmuxBackend);\n if (!stopped && processControl.isAlive(record.pid)) {\n process.emitWarning(`Could not stop runner ${record.id} during session shutdown`);\n continue;\n }\n await registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: stopped ? 'SIGTERM' : null,\n stopReason: 'session ended',\n },\n record.sessionId,\n );\n } catch (error) {\n process.emitWarning(`Could not clean up runner ${record.id}: ${String(error)}`);\n }\n }\n\n const footer = footerContribution;\n footerContribution = undefined;\n const leader = disposeLeader;\n disposeLeader = undefined;\n const context = sessionContext;\n const ownedTelemetry = telemetry;\n telemetry = undefined;\n sessionContext = undefined;\n sessionId = undefined;\n rootSessionId = undefined;\n runners = [];\n notifiedRunnerIds.clear();\n pendingPromotedRunnerIds.clear();\n\n await runCleanup('dispose the runner lifeline', () => lifeline.dispose());\n if (footer) await runCleanup('dispose the runner footer', () => footer.dispose());\n if (leader) await runCleanup('dispose the runner leader binding', leader);\n if (context?.hasUI) {\n await runCleanup('clear the runner status', () => context.ui.setStatus(RUNNER_STATUS_KEY, undefined));\n }\n await runCleanup('close the runner registry', () => registry.close());\n if (ownedTelemetry) {\n await runCleanup('record the runner session finish', () =>\n ownedTelemetry.recordEvent('doom_runner.session_finished', { outcome: 'stopped' }),\n );\n await runCleanup('stop runner telemetry', () => ownedTelemetry.shutdown());\n }\n };\n\n disposeRuntime = shutdownRuntime;\n\n footerContribution = registerDoomFooterContribution(pi, {\n source: LEADER_SOURCE,\n id: 'runner-count',\n order: RUNNER_FOOTER_ORDER,\n });\n unsubscribeRegistry = registry.subscribe(() => scheduleRefresh(false));\n statusPoll = setInterval(() => scheduleRefresh(true), RUNNER_STATUS_POLL_MS);\n statusPoll.unref?.();\n\n registerBashTool(pi, {\n bashRunService: trackedBashRunService,\n getSessionId: () => {\n if (!active || !sessionReady || !sessionId) throw new Error('doom-runner requires an active Pi session');\n return sessionId;\n },\n onRunnerStarted: (id) => {\n if (!active || !sessionReady) return;\n pendingPromotedRunnerIds.add(id);\n scheduleRefresh(false);\n },\n });\n\n registerRunnerCompactionRecovery(pi, {\n getSessionId: () => (active ? sessionId : undefined),\n listBySession: (activeSessionId) => (active ? registry.listBySession(activeSessionId) : Promise.resolve([])),\n });\n\n pi.registerCommand(COMMAND_NAME, {\n description: 'Open Runner Space: background processes started by bash',\n handler: async (_args, ctx) => {\n if (!active) return;\n const generation = sessionGeneration;\n const activeSessionId = sessionId;\n if (!ctx.hasUI) {\n ctx.ui.notify(ERR_REQUIRES_INTERACTIVE, 'error');\n return;\n }\n await refresh();\n if (!activeSessionId || !isCurrent(generation, activeSessionId)) return;\n await openRunnerSpace(ctx, {\n getRunners: () => (isCurrent(generation, activeSessionId) ? runners : []),\n getPtyRun: (name) =>\n isCurrent(generation, activeSessionId) ? (rmuxBackend.get(name) ?? ptyHost.get(name)) : undefined,\n readLog: (logPath) =>\n isCurrent(generation, activeSessionId) ? logReader.read(logPath, { lines: 1_000 }).text : '',\n stopRunner: async (id, reason) => {\n if (!isCurrent(generation, activeSessionId)) return;\n const record = runners.find((candidate) => candidate.id === id);\n if (!record) return;\n if (record.backend === RMUX_BACKEND && record.backendTarget) {\n await rmuxBackend.stop(record.backendTarget, record.pid);\n } else await launcher.stop(record.pid);\n if (!isCurrent(generation, activeSessionId)) return;\n await registry.complete(\n record.id,\n { reason: STOPPED_REASON, code: null, signal: null, stopReason: reason },\n record.sessionId,\n );\n if (isCurrent(generation, activeSessionId)) await refresh();\n },\n });\n },\n });\n\n disposeLeader = registerDoomLeaderContribution(pi, {\n source: LEADER_SOURCE,\n bindings: [\n {\n id: 'runners.open',\n path: [\n { key: 'r', label: 'runners', order: LEADER_GROUP_ORDER },\n { key: 'r', label: 'open', detail: 'background processes' },\n ],\n command: { name: COMMAND_NAME },\n },\n ],\n });\n\n pi.on(SESSION_START_EVENT, (_event, ctx) => {\n if (!active) return;\n const context = ctx as ExtensionContext;\n const activeSessionId = context.sessionManager.getSessionId();\n const previousSessionId = sessionId;\n const previousContext = sessionContext;\n const generation = ++sessionGeneration;\n sessionId = activeSessionId;\n rootSessionId = resolveRootSessionId(activeSessionId);\n sessionContext = context;\n sessionReady = false;\n lastRunnerCount = undefined;\n notifiedRunnerIds.clear();\n pendingPromotedRunnerIds.clear();\n\n const initializeSession = async (): Promise<void> => {\n if (!isCurrent(generation, activeSessionId)) return;\n if (previousSessionId && previousSessionId !== activeSessionId) {\n const previousPtyProvider = disposePtyProvider;\n disposePtyProvider = undefined;\n if (previousPtyProvider) {\n await runCleanup('unregister the previous session PTY provider', previousPtyProvider);\n }\n await runCleanup('dispose the previous session PTYs', () => ptyHost.disposeAll());\n await runCleanup('dispose the previous session lifeline', () => lifeline.dispose());\n if (previousContext?.hasUI) {\n await runCleanup('clear the previous session status', () =>\n previousContext.ui.setStatus(RUNNER_STATUS_KEY, undefined),\n );\n }\n const previousTelemetry = telemetry;\n telemetry = undefined;\n if (previousTelemetry) {\n await runCleanup('stop the previous session telemetry', () => previousTelemetry.shutdown());\n }\n if (!isCurrent(generation, activeSessionId)) return;\n }\n\n const activeTelemetry = getTelemetry(context);\n await activeTelemetry.recordEvent('doom_runner.session_started', { outcome: 'started' });\n if (!isCurrent(generation, activeSessionId)) return;\n paths.setSessionId(activeSessionId);\n // Awaited before anything can launch, so every runner this session starts\n // finds a lifeline it can connect to rather than a socket that is not\n // listening yet, which would read as an owner that is already gone.\n await lifeline.arm(activeSessionId);\n if (!isCurrent(generation, activeSessionId)) return;\n\n // This package owns the only pty in the workspace, so it lends one to any\n // extension that needs a command the user can watch and answer.\n disposePtyProvider?.();\n disposePtyProvider = provideRunnerPty(\n createProtocolRuntime({ bus: pi.events, source: LEADER_SOURCE, sessionId: activeSessionId }),\n {\n ptyHost,\n sessionId: () => (isCurrent(generation, activeSessionId) ? activeSessionId : undefined),\n onError: (error, id) => {\n if (!isCurrent(generation, activeSessionId)) return;\n void trackOperation(activeTelemetry.recordError('doom_runner.pty_launch_failed', error, { 'run.id': id }));\n },\n },\n );\n\n const retained = await registry.listAll(activeSessionId);\n if (!isCurrent(generation, activeSessionId)) return;\n for (const record of retained) {\n if (record.state === COMPLETED_STATE) notifiedRunnerIds.add(record.id);\n else if (record.promoted) pendingPromotedRunnerIds.add(record.id);\n }\n const legacy = await cleanupLegacyRunnerStore({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n paths,\n });\n if (!isCurrent(generation, activeSessionId)) return;\n const reconciled = await reconcileActiveRunners({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n startup: true,\n });\n if (!isCurrent(generation, activeSessionId)) return;\n sessionReady = true;\n await refresh(false);\n if (!isCurrent(generation, activeSessionId)) return;\n scheduleHistorySweep();\n const errors = [...legacy.errors, ...reconciled.errors];\n for (const error of errors) process.emitWarning(error);\n const reclaimedCount = legacy.reclaimed.length + reconciled.reclaimed.length;\n if (reclaimedCount > 0 && context.hasUI) {\n context.ui.notify(`Reclaimed ${reclaimedCount} stale runner record(s)`, 'warning');\n }\n await activeTelemetry.recordEvent('doom_runner.reconciled', {\n 'runner.reclaimed_count': reclaimedCount,\n 'runner.error_count': errors.length,\n outcome: errors.length === 0 ? 'completed' : 'degraded',\n });\n };\n\n const operation = lifecycleQueue.then(initializeSession, initializeSession);\n lifecycleQueue = operation.catch((error) => {\n process.emitWarning(`Could not initialize doom-runner session: ${String(error)}`);\n });\n return trackOperation(operation);\n });\n}\n\n/** The package's sole Pi factory; Pi reloads it and Cordis owns all package resources. */\nexport async function runnerExtension(pi: ExtensionAPI): Promise<void> {\n const cordis = new Context();\n let disposal: Promise<void> | undefined;\n const dispose = (): Promise<void> => {\n disposal ??= cordis.fiber.dispose();\n return disposal;\n };\n\n try {\n installRunnerRuntime(cordis, pi);\n pi.on(SESSION_SHUTDOWN_EVENT, dispose);\n } catch (error) {\n await dispose();\n throw error;\n }\n}\n\nexport default runnerExtension;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"extension.mjs","names":[],"sources":["../../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport { connectDoomCordisHost } from '@agimon-ai/doompi-extension-contracts/cordis-host';\nimport {\n createDoomReadinessCoordinator,\n type DoomReadinessCoordinator,\n readDoomReadinessCoordinator,\n} from '@agimon-ai/doompi-extension-contracts/readiness';\nimport type { DoomFooterContributionHandle } from '@agimon-ai/doompi-extension-contracts/footer';\nimport { DOOM_UI_HUB_SERVICE, requireDoomUiHub } from '@agimon-ai/doompi-extension-contracts/ui-hub';\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { registerBashTool } from '../../commands/bash/bashTool.ts';\nimport { createRunnerContainer } from '../../container/index.ts';\nimport type { IBashRunService } from '../../types/bashRunService';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport { registerRunnerCompactionRecovery } from '../../services/runs/compaction.ts';\nimport { cleanupLegacyRunnerStore, reconcileActiveRunners, stopRunnerProcess } from '../../services/runs/reconcile.ts';\nimport { formatRunnerFooterContribution, formatRunnerStatus } from '../../tui/format.ts';\nimport { openRunnerSpace } from '../../tui/runnerSpace.ts';\nimport { getLogTtlMs } from '../../types/config.ts';\n\nconst LEADER_SOURCE = '@agimon-ai/doompi-runner';\n/** After doom-task's `t` (65) and before the core help group (70). */\nconst LEADER_GROUP_ORDER = 67;\nconst COMMAND_NAME = 'runners';\nconst ERR_REQUIRES_INTERACTIVE = '/runners requires interactive mode';\n\nconst SESSION_START_EVENT = 'session_start';\nconst RUNNER_FINISHED_MESSAGE = 'doom-runner-finished';\nconst COMPLETED_STATE = 'completed';\nconst RMUX_BACKEND = 'rmux';\nconst STOPPED_REASON = 'stopped';\nconst RUNNER_STATUS_KEY = 'doom-runner-runners';\nconst RUNNER_FOOTER_ORDER = 10;\nconst RUNNER_STATUS_POLL_MS = 500;\n\n/**\n * doom-runner: replaces pi's `bash` tool with a supervised one and provides a\n * CLI for anything it leaves running.\n */\nexport function installRunnerRuntime(cordis: Context, pi: ExtensionAPI): void {\n const container = createRunnerContainer();\n const registry = container.runnerRegistry;\n let disposeRuntime = async (): Promise<void> => {\n try {\n registry.close();\n } catch (error) {\n process.emitWarning(`Could not close a partially installed doom-runner registry: ${String(error)}`);\n }\n };\n cordis.effect(() => () => disposeRuntime(), `${LEADER_SOURCE}/runtime`);\n\n const launcher = container.launcher;\n const rmuxBackend = container.rmuxBackend;\n const logReader = container.logReader;\n const ptyHost = container.ptyHost;\n const bashRunService = container.bashRunService;\n const paths = container.paths;\n const processControl = container.processControl;\n const lifeline = container.lifeline;\n\n let active = true;\n let sessionGeneration = 0;\n let sessionId: string | undefined;\n let rootSessionId: string | undefined;\n let runners: RunnerRecord[] = [];\n let sessionContext: ExtensionContext | undefined;\n let footerContribution: DoomFooterContributionHandle | undefined;\n let telemetry: DoomTelemetry | undefined;\n let sessionReady = false;\n let disposed = false;\n let refreshInFlight: Promise<void> | undefined;\n let historySweepInFlight: Promise<void> | undefined;\n let historySweepTimer: ReturnType<typeof setImmediate> | undefined;\n let statusPoll: ReturnType<typeof setInterval> | undefined;\n let unsubscribeRegistry: (() => void) | undefined;\n let sessionInitialization: Promise<void> = Promise.resolve();\n let fallbackReadiness: DoomReadinessCoordinator | undefined;\n let refreshQueued = false;\n let queuedReconciliation = false;\n let lastRunnerCount: number | undefined;\n const notifiedRunnerIds = new Set<string>();\n const pendingPromotedRunnerIds = new Set<string>();\n const pendingOperations = new Set<Promise<unknown>>();\n const trackOperation = <T>(operation: Promise<T>): Promise<T> => {\n pendingOperations.add(operation);\n void operation.then(\n () => pendingOperations.delete(operation),\n () => pendingOperations.delete(operation),\n );\n return operation;\n };\n const waitForSessionReadiness = async (): Promise<void> => {\n await sessionInitialization;\n if (!active || !sessionReady || !sessionId) {\n throw new Error('doom-runner requires an active Pi session');\n }\n };\n const trackedBashRunService: IBashRunService = {\n run: async (request) => {\n await waitForSessionReadiness();\n return trackOperation(bashRunService.run(request));\n },\n };\n const isCurrent = (generation: number, expectedSessionId = sessionId): boolean =>\n active && !disposed && generation === sessionGeneration && expectedSessionId === sessionId;\n const getTelemetry = (ctx: ExtensionContext): DoomTelemetry => {\n telemetry ??= createDoomTelemetry({\n serviceName: 'doom-runner',\n packageName: '@agimon-ai/doompi-runner',\n cwd: ctx.cwd,\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n return telemetry;\n };\n /** Performs one bounded pass over active state and explicitly monitored runners. */\n const refreshNow = async (shouldReconcile: boolean): Promise<void> => {\n const activeSessionId = sessionId;\n const generation = sessionGeneration;\n if (!activeSessionId || !isCurrent(generation, activeSessionId)) return;\n\n const activeRecords = await registry.list();\n if (!isCurrent(generation, activeSessionId)) return;\n let visibleActive = activeRecords;\n if (shouldReconcile) {\n const reconciled = await reconcileActiveRunners({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n startup: false,\n active: activeRecords,\n });\n for (const error of reconciled.errors) process.emitWarning(error);\n if (reconciled.reclaimed.length > 0) {\n const reclaimedIds = new Set(reconciled.reclaimed);\n visibleActive = activeRecords.filter((record) => !reclaimedIds.has(record.id));\n }\n }\n if (!isCurrent(generation, activeSessionId)) return;\n\n runners = visibleActive.filter((record) => record.sessionId === activeSessionId);\n const rootRunners = rootSessionId\n ? visibleActive.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId)\n : [];\n const runnerCount = rootRunners.length;\n if (lastRunnerCount !== runnerCount) {\n lastRunnerCount = runnerCount;\n footerContribution?.update(formatRunnerFooterContribution(runnerCount));\n if (sessionContext?.hasUI) sessionContext.ui.setStatus(RUNNER_STATUS_KEY, formatRunnerStatus(runnerCount));\n }\n\n const pendingIds = [...pendingPromotedRunnerIds];\n const monitored = await Promise.all(pendingIds.map((id) => registry.get(id, activeSessionId)));\n if (!isCurrent(generation, activeSessionId)) return;\n for (const [index, record] of monitored.entries()) {\n const id = pendingIds[index];\n if (!id) continue;\n if (!record || record.sessionId !== activeSessionId) {\n pendingPromotedRunnerIds.delete(id);\n continue;\n }\n if (record.state !== COMPLETED_STATE) continue;\n pendingPromotedRunnerIds.delete(record.id);\n if (notifiedRunnerIds.has(record.id)) continue;\n notifiedRunnerIds.add(record.id);\n const outcome = record.exit?.reason ?? COMPLETED_STATE;\n const code =\n record.exit?.code === null || record.exit?.code === undefined ? '' : `, exit code ${record.exit.code}`;\n const content = [\n `Background runner ${record.name} exited: ${outcome}${code}.`,\n `Runner ID: ${record.id}`,\n `Log: ${record.logPath}`,\n `Inspect: doom-runner logs ${record.id}`,\n ].join('\\n');\n pi.sendMessage(\n { customType: RUNNER_FINISHED_MESSAGE, content, display: true },\n { triggerTurn: true, deliverAs: 'steer' },\n );\n if (sessionContext) {\n void trackOperation(\n getTelemetry(sessionContext).recordEvent('doom_runner.process_finished', {\n outcome: 'completed',\n 'runner.exit_code': record.exit?.code ?? 0,\n 'runner.backend': record.backend,\n ...(Number.isFinite(Date.parse(record.startedAt))\n ? { duration_ms: Math.max(0, Date.now() - Date.parse(record.startedAt)) }\n : {}),\n }),\n );\n }\n }\n };\n\n /** Coalesces timer and event triggers without losing a requested reconciliation pass. */\n const refresh = (shouldReconcile = true): Promise<void> => {\n if (!sessionId || !sessionReady || disposed) return Promise.resolve();\n if (refreshInFlight) {\n refreshQueued = true;\n queuedReconciliation ||= shouldReconcile;\n return refreshInFlight;\n }\n\n const execution = (async () => {\n let reconcileNext = shouldReconcile;\n do {\n refreshQueued = false;\n queuedReconciliation = false;\n await refreshNow(reconcileNext);\n reconcileNext = queuedReconciliation;\n } while (refreshQueued && !disposed);\n })().finally(() => {\n if (refreshInFlight === execution) refreshInFlight = undefined;\n });\n refreshInFlight = execution;\n return execution;\n };\n\n const reportRefreshError = (error: unknown): void => {\n if (disposed) return;\n if (sessionContext) {\n void trackOperation(getTelemetry(sessionContext).recordError('doom_runner.refresh_failed', error));\n }\n process.emitWarning(`Could not refresh doom-runner: ${String(error)}`);\n };\n const scheduleRefresh = (shouldReconcile: boolean): void => {\n void refresh(shouldReconcile).catch(reportRefreshError);\n };\n const scheduleHistorySweep = (): void => {\n if (historySweepTimer || historySweepInFlight || disposed) return;\n historySweepTimer = setImmediate(() => {\n historySweepTimer = undefined;\n if (disposed) return;\n const execution = (async () => {\n const sweep = paths.sweepHistoryAsync\n ? await paths.sweepHistoryAsync(getLogTtlMs())\n : paths.sweepHistory(getLogTtlMs());\n for (const error of sweep.errors) process.emitWarning(error);\n })()\n .catch((error) => process.emitWarning(`Could not sweep doom-runner history: ${String(error)}`))\n .finally(() => {\n if (historySweepInFlight === execution) historySweepInFlight = undefined;\n });\n historySweepInFlight = execution;\n });\n historySweepTimer.unref?.();\n };\n\n const runCleanup = async (label: string, cleanup: () => void | Promise<void>): Promise<void> => {\n try {\n await cleanup();\n } catch (error) {\n process.emitWarning(`Doom-runner cleanup could not ${label}: ${String(error)}`);\n }\n };\n\n const shutdownRuntime = async (): Promise<void> => {\n if (disposed) return;\n active = false;\n disposed = true;\n sessionGeneration += 1;\n sessionReady = false;\n if (statusPoll) clearInterval(statusPoll);\n statusPoll = undefined;\n if (historySweepTimer) clearImmediate(historySweepTimer);\n historySweepTimer = undefined;\n const unsubscribe = unsubscribeRegistry;\n unsubscribeRegistry = undefined;\n if (unsubscribe) await runCleanup('unsubscribe from the runner registry', unsubscribe);\n\n const ownedReadiness = fallbackReadiness;\n fallbackReadiness = undefined;\n if (ownedReadiness) await runCleanup('cancel standalone readiness work', () => ownedReadiness.dispose());\n await Promise.allSettled(pendingOperations);\n const activeRefresh = refreshInFlight;\n if (activeRefresh) await runCleanup('settle the runner refresh', () => activeRefresh);\n const activeHistorySweep = historySweepInFlight;\n if (activeHistorySweep) await runCleanup('settle runner history cleanup', () => activeHistorySweep);\n\n await runCleanup('dispose runner PTYs', () => ptyHost.disposeAll());\n\n let owned: RunnerRecord[] = [];\n if (sessionId) {\n try {\n owned = await registry.listBySession(sessionId);\n } catch (error) {\n process.emitWarning(`Could not list runners during doom-runner shutdown: ${String(error)}`);\n }\n }\n await Promise.all(\n owned.map(async (record) => {\n try {\n const stopped = await stopRunnerProcess(record, launcher, rmuxBackend);\n if (!stopped && processControl.isAlive(record.pid)) {\n process.emitWarning(`Could not stop runner ${record.id} during session shutdown`);\n return;\n }\n await registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: stopped ? 'SIGTERM' : null,\n stopReason: 'session ended',\n },\n record.sessionId,\n );\n } catch (error) {\n process.emitWarning(`Could not clean up runner ${record.id}: ${String(error)}`);\n }\n }),\n );\n\n const context = sessionContext;\n const ownedTelemetry = telemetry;\n telemetry = undefined;\n sessionContext = undefined;\n sessionId = undefined;\n rootSessionId = undefined;\n runners = [];\n notifiedRunnerIds.clear();\n pendingPromotedRunnerIds.clear();\n\n await Promise.all([\n runCleanup('dispose the runner lifeline', () => lifeline.dispose()),\n ...(context?.hasUI\n ? [runCleanup('clear the runner status', () => context.ui.setStatus(RUNNER_STATUS_KEY, undefined))]\n : []),\n ]);\n await runCleanup('close the runner registry', () => registry.close());\n if (ownedTelemetry) {\n await runCleanup('record the runner session finish', () =>\n ownedTelemetry.recordEvent('doom_runner.session_finished', { outcome: 'stopped' }),\n );\n await runCleanup('stop runner telemetry', () => ownedTelemetry.shutdown());\n }\n };\n\n disposeRuntime = shutdownRuntime;\n\n cordis.inject([DOOM_UI_HUB_SERVICE], (uiContext) => {\n const contribution = requireDoomUiHub(uiContext).registerFooter({\n source: LEADER_SOURCE,\n id: 'runner-count',\n order: RUNNER_FOOTER_ORDER,\n });\n footerContribution = contribution;\n return () => {\n contribution.dispose();\n if (footerContribution === contribution) footerContribution = undefined;\n };\n });\n unsubscribeRegistry = registry.subscribe(() => scheduleRefresh(false));\n statusPoll = setInterval(() => scheduleRefresh(true), RUNNER_STATUS_POLL_MS);\n statusPoll.unref?.();\n\n registerBashTool(pi, {\n bashRunService: trackedBashRunService,\n getSessionId: async () => {\n await waitForSessionReadiness();\n if (!sessionId) throw new Error('doom-runner requires an active Pi session');\n return sessionId;\n },\n onRunnerStarted: (id) => {\n if (!active || !sessionReady) return;\n pendingPromotedRunnerIds.add(id);\n scheduleRefresh(false);\n },\n });\n\n registerRunnerCompactionRecovery(pi, {\n getSessionId: async () => {\n try {\n await waitForSessionReadiness();\n return sessionId;\n } catch {\n return undefined;\n }\n },\n listBySession: async (activeSessionId) => {\n try {\n await waitForSessionReadiness();\n } catch {\n return [];\n }\n return active ? registry.listBySession(activeSessionId) : [];\n },\n });\n\n pi.registerCommand(COMMAND_NAME, {\n description: 'Open Runner Space: background processes started by bash',\n handler: async (_args, ctx) => {\n if (!active) return;\n const generation = sessionGeneration;\n const activeSessionId = sessionId;\n if (!activeSessionId) return;\n if (!ctx.hasUI) {\n ctx.ui.notify(ERR_REQUIRES_INTERACTIVE, 'error');\n return;\n }\n await waitForSessionReadiness();\n await refresh();\n if (!activeSessionId || !isCurrent(generation, activeSessionId)) return;\n await openRunnerSpace(ctx, {\n getRunners: () => (isCurrent(generation, activeSessionId) ? runners : []),\n getPtyRun: (name) =>\n isCurrent(generation, activeSessionId) ? (rmuxBackend.get(name) ?? ptyHost.get(name)) : undefined,\n readLog: (logPath) =>\n isCurrent(generation, activeSessionId) ? logReader.read(logPath, { lines: 1_000 }).text : '',\n stopRunner: async (id, reason) => {\n if (!isCurrent(generation, activeSessionId)) return;\n const record = runners.find((candidate) => candidate.id === id);\n if (!record) return;\n if (record.backend === RMUX_BACKEND && record.backendTarget) {\n await rmuxBackend.stop(record.backendTarget, record.pid);\n } else await launcher.stop(record.pid);\n if (!isCurrent(generation, activeSessionId)) return;\n await registry.complete(\n record.id,\n { reason: STOPPED_REASON, code: null, signal: null, stopReason: reason },\n record.sessionId,\n );\n if (isCurrent(generation, activeSessionId)) await refresh();\n },\n });\n },\n });\n\n cordis.inject([DOOM_UI_HUB_SERVICE], (uiContext) => {\n const contribution = requireDoomUiHub(uiContext).registerLeader({\n source: LEADER_SOURCE,\n bindings: [\n {\n id: 'runners.open',\n path: [\n { key: 'r', label: 'runners', order: LEADER_GROUP_ORDER },\n { key: 'r', label: 'open', detail: 'background processes' },\n ],\n command: { name: COMMAND_NAME },\n },\n ],\n });\n return () => contribution.dispose();\n });\n\n pi.on(SESSION_START_EVENT, (_event, ctx) => {\n if (!active) return;\n const context = ctx as ExtensionContext;\n const activeSessionId = context.sessionManager.getSessionId();\n const previousSessionId = sessionId;\n const previousContext = sessionContext;\n const generation = ++sessionGeneration;\n sessionId = activeSessionId;\n rootSessionId = resolveRootSessionId(activeSessionId);\n sessionContext = context;\n sessionReady = false;\n lastRunnerCount = undefined;\n notifiedRunnerIds.clear();\n pendingPromotedRunnerIds.clear();\n\n const initializeSession = async (signal: AbortSignal): Promise<readonly string[]> => {\n const stillCurrent = (): boolean => !signal.aborted && isCurrent(generation, activeSessionId);\n if (!stillCurrent()) return [];\n if (previousSessionId && previousSessionId !== activeSessionId) {\n await runCleanup('dispose the previous session PTYs', () => ptyHost.disposeAll());\n await runCleanup('dispose the previous session lifeline', () => lifeline.dispose());\n if (previousContext?.hasUI) {\n await runCleanup('clear the previous session status', () =>\n previousContext.ui.setStatus(RUNNER_STATUS_KEY, undefined),\n );\n }\n const previousTelemetry = telemetry;\n telemetry = undefined;\n if (previousTelemetry) {\n await runCleanup('stop the previous session telemetry', () => previousTelemetry.shutdown());\n }\n if (!stillCurrent()) return [];\n }\n\n const activeTelemetry = getTelemetry(context);\n await activeTelemetry.recordEvent('doom_runner.session_started', { outcome: 'started' });\n if (!stillCurrent()) return [];\n paths.setSessionId(activeSessionId);\n // Awaited before anything can launch, so every runner this session starts\n // finds a lifeline it can connect to rather than a socket that is not\n // listening yet, which would read as an owner that is already gone.\n await lifeline.arm(activeSessionId);\n if (!stillCurrent()) return [];\n\n const retained = await registry.listAll(activeSessionId);\n if (!stillCurrent()) return [];\n for (const record of retained) {\n if (record.state === COMPLETED_STATE) notifiedRunnerIds.add(record.id);\n else if (record.promoted) pendingPromotedRunnerIds.add(record.id);\n }\n const legacy = await cleanupLegacyRunnerStore({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n paths,\n });\n if (!stillCurrent()) return [];\n const reconciled = await reconcileActiveRunners({\n registry,\n launcher,\n rmuxBackend,\n processControl,\n currentHostPid: process.pid,\n startup: true,\n });\n if (!stillCurrent()) return [];\n sessionReady = true;\n await refresh(false);\n if (!stillCurrent()) return [];\n scheduleHistorySweep();\n const errors = [...legacy.errors, ...reconciled.errors];\n const reclaimedCount = legacy.reclaimed.length + reconciled.reclaimed.length;\n if (reclaimedCount > 0 && context.hasUI) {\n context.ui.notify(`Reclaimed ${reclaimedCount} stale runner record(s)`, 'warning');\n }\n await activeTelemetry.recordEvent('doom_runner.reconciled', {\n 'runner.reclaimed_count': reclaimedCount,\n 'runner.error_count': errors.length,\n outcome: errors.length === 0 ? 'completed' : 'degraded',\n });\n return errors;\n };\n\n const previousInitialization = sessionInitialization.catch(() => undefined);\n const startReadiness = async (): Promise<void> => {\n await previousInitialization;\n if (!isCurrent(generation, activeSessionId)) return;\n const coordinator =\n readDoomReadinessCoordinator(cordis) ??\n (fallbackReadiness ??= createDoomReadinessCoordinator({\n notify: (notification) => {\n process.emitWarning(\n `${notification.packageId} initialization ${notification.state}: ${notification.error?.message ?? notification.diagnostics.join('; ')}`,\n );\n },\n }));\n const handle = coordinator.start(LEADER_SOURCE, `${activeSessionId}:${generation}`, async (signal) => ({\n value: undefined,\n diagnostics: await initializeSession(signal),\n }));\n await handle.wait();\n };\n const operation = startReadiness();\n sessionInitialization = operation;\n // The coordinator reports a failed generation once; this branch only marks\n // the detached waiter handled so Pi is not held open by the notification path.\n void trackOperation(operation).catch(() => undefined);\n });\n}\n\n/** The package's sole Pi factory; Pi reloads it and Cordis owns all package resources. */\nexport async function runnerExtension(pi: ExtensionAPI): Promise<void> {\n const connection = await connectDoomCordisHost(pi, LEADER_SOURCE);\n const fiber = connection.root.plugin(runnerPlugin, { pi });\n try {\n await fiber;\n } catch (error) {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n throw error;\n }\n let disposal: Promise<void> | undefined;\n pi.on(\n 'session_shutdown',\n () =>\n (disposal ??= (async () => {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n })()),\n );\n}\n\ninterface RunnerPluginConfig {\n readonly pi: ExtensionAPI;\n}\n\nfunction runnerPlugin(cordis: Context, config: RunnerPluginConfig): void {\n installRunnerRuntime(cordis, config.pi);\n}\n\nexport default runnerExtension;\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const e=require("../../types/config.cjs"),t=require("../../services/AnsiScrub/ansiScrub.cjs"),n=require("./responseEnvelope.cjs"),r=require("../../schemas/bashTool.cjs"),i=require("../../tui/bashRender.cjs"),a=1e3,o=`Options:`,s={requested:`Started in the background`,threshold:`Still running after the background threshold, so it was moved to the background`,interactive:`Started on a terminal so it can prompt, which means it runs in the background`},c=`Execute bash commands, with long-running ones supervised in the background`;function l(t=e.getBackgroundThresholdMs()){return[`A command still running after ${Math.round(t/a)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,`Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.`,`Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.`,`Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.`,`Stop runners you no longer need. Every runner is stopped automatically when the session ends.`]}function u(e,t){e.registerTool({name:r.BASH_TOOL_NAME,label:r.BASH_TOOL_LABEL,description:`Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.`,promptSnippet:c,promptGuidelines:l(),parameters:r.BashParamsSchema,renderShell:`self`,async execute(e,r,i,s,c){let{command:l,timeout:u,background:f,interactive:p,name:m}=r,h;s&&s(n.textResult(`Starting ${p===!0?`interactive runner`:f===!0?`background runner`:`command`}...`)),f!==!0&&p!==!0&&s&&(h=e=>s(n.textResult(e)));let g;try{g=await t.bashRunService.run({command:l,timeoutMs:u===void 0?void 0:u*a,background:f,interactive:p,name:m,...h?{onOutput:h}:{},sessionId:t.getSessionId()})}catch(e){let t=e instanceof Error?e.message:String(e);throw Error([`Could not execute the command: ${t}`,``,o,`- Check the command, runtime, and working directory, then retry once.`,`- Use a read-only diagnostic to confirm the missing dependency or environment value.`,`- Ask the user if recovery requires changing the requested approach.`].join(`
|
|
1
|
+
const e=require("../../types/config.cjs"),t=require("../../services/AnsiScrub/ansiScrub.cjs"),n=require("./responseEnvelope.cjs"),r=require("../../schemas/bashTool.cjs"),i=require("../../tui/bashRender.cjs"),a=1e3,o=`Options:`,s={requested:`Started in the background`,threshold:`Still running after the background threshold, so it was moved to the background`,interactive:`Started on a terminal so it can prompt, which means it runs in the background`},c=`Execute bash commands, with long-running ones supervised in the background`;function l(t=e.getBackgroundThresholdMs()){return[`A command still running after ${Math.round(t/a)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,`Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.`,`Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.`,`Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.`,`Stop runners you no longer need. Every runner is stopped automatically when the session ends.`]}function u(e,t){e.registerTool({name:r.BASH_TOOL_NAME,label:r.BASH_TOOL_LABEL,description:`Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.`,promptSnippet:c,promptGuidelines:l(),parameters:r.BashParamsSchema,renderShell:`self`,async execute(e,r,i,s,c){let{command:l,timeout:u,background:f,interactive:p,name:m}=r,h;s&&s(n.textResult(`Starting ${p===!0?`interactive runner`:f===!0?`background runner`:`command`}...`)),f!==!0&&p!==!0&&s&&(h=e=>s(n.textResult(e)));let g;try{g=await t.bashRunService.run({command:l,timeoutMs:u===void 0?void 0:u*a,background:f,interactive:p,name:m,...h?{onOutput:h}:{},sessionId:await t.getSessionId()})}catch(e){let t=e instanceof Error?e.message:String(e);throw Error([`Could not execute the command: ${t}`,``,o,`- Check the command, runtime, and working directory, then retry once.`,`- Use a read-only diagnostic to confirm the missing dependency or environment value.`,`- Ask the user if recovery requires changing the requested approach.`].join(`
|
|
2
2
|
`),{cause:e})}return g.kind===`promoted`&&t.onRunnerStarted(g.id),d(g)},renderCall(e,t,n){return i.renderBashCall(e,t)},renderResult(e,t,n,r){return i.renderBashResult(e,{...t,isError:r.isError},n)}})}function d(e){if(e.kind===`failed`)throw Error([`Could not start runner "${e.name}": ${e.error}`,``,o,`- Correct the command or missing executable, then retry once.`,`- Use a read-only diagnostic to inspect the runtime environment.`,`- Ask the user if a different execution approach is required.`].join(`
|
|
3
3
|
`));if(e.kind===`promoted`){let t=[`${s[e.reason]} as runner "${e.name}".`,`Runner ID: ${e.id}`,`Backend: ${e.backend}`,`Log (streaming): ${e.logPath}`,`Inspect it with: doom-runner logs ${e.id}`].filter(e=>e.length>0).join(`
|
|
4
4
|
`);return{...n.textResult(t,{id:e.id,runner:e.name,pid:e.pid,logPath:e.logPath,promoted:!0,reason:e.reason})}}let r=n.summarizeLog(e.logPath),i=[];e.timedOut?i.push(`Command stopped: it exceeded the requested timeout.`):e.exitCode===null?e.signal!==null&&i.push(`Terminated by ${e.signal}`):i.push(`Exit: completed with code ${e.exitCode}.`);let a=t.stripAnsi(r.tail),c=[a.length>0?`Log tail (${r.tailLines} lines):\n${a}`:`Log tail: (no output)`,`Runner ID: ${e.id}`,...i,`Log: ${e.logPath}`,`File size: ${n.formatSize(r.bytes)}`,`Lines: ${r.lines.toLocaleString(`en-US`)}`].join(`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashTool.cjs","names":["getBackgroundThresholdMs","BASH_TOOL_NAME","BASH_TOOL_LABEL","BashParamsSchema","textResult","renderBashCall","renderBashResult","summarizeLog","stripAnsi","formatSize"],"sources":["../../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/ansiScrub';\nimport type { BashRunResult, IBashRunService } from '../../types/bashRunService';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport { getBackgroundThresholdMs } from '../../types/config.ts';\nimport { formatSize, summarizeLog, type ToolResult, textResult } from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\nconst ERROR_OPTIONS_HEADING = 'Options:';\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold, so it was moved to the background',\n interactive: 'Started on a terminal so it can prompt, which means it runs in the background',\n};\n\nexport const BASH_PROMPT_SNIPPET = 'Execute bash commands, with long-running ones supervised in the background';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,\n 'Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.',\n 'Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.',\n 'Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.',\n 'Stop runners you no longer need. Every runner is stopped automatically when the session ends.',\n ];\n}\n\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, _signal, onUpdate, _ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n sessionId: dependencies.getSessionId(),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute the command: ${message}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Check the command, runtime, and working directory, then retry once.',\n '- Use a read-only diagnostic to confirm the missing dependency or environment value.',\n '- Ask the user if recovery requires changing the requested approach.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result);\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, context) {\n return renderBashResult(result, { ...options, isError: context.isError }, theme);\n },\n });\n}\n\nexport function formatRunResult(result: BashRunResult): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Correct the command or missing executable, then retry once.',\n '- Use a read-only diagnostic to inspect the runtime environment.',\n '- Ask the user if a different execution approach is required.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason} as runner \"${result.name}\".`,\n `Runner ID: ${result.id}`,\n `Backend: ${result.backend}`,\n `Log (streaming): ${result.logPath}`,\n `Inspect it with: doom-runner logs ${result.id}`,\n ]\n .filter((line) => line.length > 0)\n .join('\\n');\n\n return {\n ...textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n }),\n };\n }\n\n const log = summarizeLog(result.logPath);\n const notes: string[] = [];\n if (result.timedOut) notes.push('Command stopped: it exceeded the requested timeout.');\n else if (result.exitCode !== null) notes.push(`Exit: completed with code ${result.exitCode}.`);\n else if (result.signal !== null) notes.push(`Terminated by ${result.signal}`);\n\n // The model reads plain text; only the renderer gets the coloured copy below.\n const plainTail = stripAnsi(log.tail);\n const text = [\n plainTail.length > 0 ? `Log tail (${log.tailLines} lines):\\n${plainTail}` : 'Log tail: (no output)',\n `Runner ID: ${result.id}`,\n ...notes,\n `Log: ${result.logPath}`,\n `File size: ${formatSize(log.bytes)}`,\n `Lines: ${log.lines.toLocaleString('en-US')}`,\n ].join('\\n');\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: log.lines,\n // Carried structurally so renderResult shows the output instead of the footer.\n tail: log.tail,\n tailLines: log.tailLines,\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n if (result.timedOut || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0)) {\n throw new Error(\n [\n text,\n '',\n ERROR_OPTIONS_HEADING,\n `- Inspect the full log at ${result.logPath}, correct the cause, then retry once.`,\n '- Run a read-only diagnostic if the output does not explain the failure.',\n '- Ask the user if the failure requires changing the requested approach.',\n ].join('\\n'),\n );\n }\n return textResult(text, details);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bashTool.cjs","names":["getBackgroundThresholdMs","BASH_TOOL_NAME","BASH_TOOL_LABEL","BashParamsSchema","textResult","renderBashCall","renderBashResult","summarizeLog","stripAnsi","formatSize"],"sources":["../../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/ansiScrub';\nimport type { BashRunResult, IBashRunService } from '../../types/bashRunService';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport { getBackgroundThresholdMs } from '../../types/config.ts';\nimport { formatSize, summarizeLog, type ToolResult, textResult } from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\nconst ERROR_OPTIONS_HEADING = 'Options:';\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold, so it was moved to the background',\n interactive: 'Started on a terminal so it can prompt, which means it runs in the background',\n};\n\nexport const BASH_PROMPT_SNIPPET = 'Execute bash commands, with long-running ones supervised in the background';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,\n 'Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.',\n 'Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.',\n 'Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.',\n 'Stop runners you no longer need. Every runner is stopped automatically when the session ends.',\n ];\n}\n\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string | Promise<string>;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, _signal, onUpdate, _ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n sessionId: await dependencies.getSessionId(),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute the command: ${message}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Check the command, runtime, and working directory, then retry once.',\n '- Use a read-only diagnostic to confirm the missing dependency or environment value.',\n '- Ask the user if recovery requires changing the requested approach.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result);\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, context) {\n return renderBashResult(result, { ...options, isError: context.isError }, theme);\n },\n });\n}\n\nexport function formatRunResult(result: BashRunResult): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Correct the command or missing executable, then retry once.',\n '- Use a read-only diagnostic to inspect the runtime environment.',\n '- Ask the user if a different execution approach is required.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason} as runner \"${result.name}\".`,\n `Runner ID: ${result.id}`,\n `Backend: ${result.backend}`,\n `Log (streaming): ${result.logPath}`,\n `Inspect it with: doom-runner logs ${result.id}`,\n ]\n .filter((line) => line.length > 0)\n .join('\\n');\n\n return {\n ...textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n }),\n };\n }\n\n const log = summarizeLog(result.logPath);\n const notes: string[] = [];\n if (result.timedOut) notes.push('Command stopped: it exceeded the requested timeout.');\n else if (result.exitCode !== null) notes.push(`Exit: completed with code ${result.exitCode}.`);\n else if (result.signal !== null) notes.push(`Terminated by ${result.signal}`);\n\n // The model reads plain text; only the renderer gets the coloured copy below.\n const plainTail = stripAnsi(log.tail);\n const text = [\n plainTail.length > 0 ? `Log tail (${log.tailLines} lines):\\n${plainTail}` : 'Log tail: (no output)',\n `Runner ID: ${result.id}`,\n ...notes,\n `Log: ${result.logPath}`,\n `File size: ${formatSize(log.bytes)}`,\n `Lines: ${log.lines.toLocaleString('en-US')}`,\n ].join('\\n');\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: log.lines,\n // Carried structurally so renderResult shows the output instead of the footer.\n tail: log.tail,\n tailLines: log.tailLines,\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n if (result.timedOut || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0)) {\n throw new Error(\n [\n text,\n '',\n ERROR_OPTIONS_HEADING,\n `- Inspect the full log at ${result.logPath}, correct the cause, then retry once.`,\n '- Run a read-only diagnostic if the output does not explain the failure.',\n '- Ask the user if the failure requires changing the requested approach.',\n ].join('\\n'),\n );\n }\n return textResult(text, details);\n}\n"],"mappings":"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"}
|
|
@@ -7,7 +7,7 @@ declare const BASH_PROMPT_SNIPPET = "Execute bash commands, with long-running on
|
|
|
7
7
|
declare function bashPromptGuidelines(thresholdMs?: number): string[];
|
|
8
8
|
interface BashToolDependencies {
|
|
9
9
|
bashRunService: IBashRunService;
|
|
10
|
-
getSessionId(): string
|
|
10
|
+
getSessionId(): string | Promise<string>;
|
|
11
11
|
/** Called after a runner is promoted, so UI state can refresh. */
|
|
12
12
|
onRunnerStarted(id: string): void;
|
|
13
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashTool.d.cts","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"mappings":";;;;cAiBa;;iBAGG,qBAAqB;UAUpB;EACf,gBAAgB;EAChB;;
|
|
1
|
+
{"version":3,"file":"bashTool.d.cts","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"mappings":";;;;cAiBa;;iBAGG,qBAAqB;UAUpB;EACf,gBAAgB;EAChB,yBAAyB;;EAEzB,gBAAgB;;;;;;;;iBASF,iBAAiB,IAAI,cAAc,cAAc;iBAmEjD,gBAAgB,QAAQ,gBAAgB"}
|
|
@@ -7,7 +7,7 @@ declare const BASH_PROMPT_SNIPPET = "Execute bash commands, with long-running on
|
|
|
7
7
|
declare function bashPromptGuidelines(thresholdMs?: number): string[];
|
|
8
8
|
interface BashToolDependencies {
|
|
9
9
|
bashRunService: IBashRunService;
|
|
10
|
-
getSessionId(): string
|
|
10
|
+
getSessionId(): string | Promise<string>;
|
|
11
11
|
/** Called after a runner is promoted, so UI state can refresh. */
|
|
12
12
|
onRunnerStarted(id: string): void;
|
|
13
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashTool.d.mts","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"mappings":";;;;cAiBa;;iBAGG,qBAAqB;UAUpB;EACf,gBAAgB;EAChB;;
|
|
1
|
+
{"version":3,"file":"bashTool.d.mts","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"mappings":";;;;cAiBa;;iBAGG,qBAAqB;UAUpB;EACf,gBAAgB;EAChB,yBAAyB;;EAEzB,gBAAgB;;;;;;;;iBASF,iBAAiB,IAAI,cAAc,cAAc;iBAmEjD,gBAAgB,QAAQ,gBAAgB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{getBackgroundThresholdMs as e}from"../../types/config.mjs";import{stripAnsi as t}from"../../services/AnsiScrub/ansiScrub.mjs";import{formatSize as n,summarizeLog as r,textResult as i}from"./responseEnvelope.mjs";import{BASH_TOOL_LABEL as a,BASH_TOOL_NAME as o,BashParamsSchema as s}from"../../schemas/bashTool.mjs";import{renderBashCall as c,renderBashResult as l}from"../../tui/bashRender.mjs";const u=1e3,d=`Options:`,f={requested:`Started in the background`,threshold:`Still running after the background threshold, so it was moved to the background`,interactive:`Started on a terminal so it can prompt, which means it runs in the background`},p=`Execute bash commands, with long-running ones supervised in the background`;function m(t=e()){return[`A command still running after ${Math.round(t/u)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,`Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.`,`Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.`,`Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.`,`Stop runners you no longer need. Every runner is stopped automatically when the session ends.`]}function h(e,t){e.registerTool({name:o,label:a,description:`Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.`,promptSnippet:p,promptGuidelines:m(),parameters:s,renderShell:`self`,async execute(e,n,r,a,o){let{command:s,timeout:c,background:l,interactive:f,name:p}=n,m;a&&a(i(`Starting ${f===!0?`interactive runner`:l===!0?`background runner`:`command`}...`)),l!==!0&&f!==!0&&a&&(m=e=>a(i(e)));let h;try{h=await t.bashRunService.run({command:s,timeoutMs:c===void 0?void 0:c*u,background:l,interactive:f,name:p,...m?{onOutput:m}:{},sessionId:t.getSessionId()})}catch(e){let t=e instanceof Error?e.message:String(e);throw Error([`Could not execute the command: ${t}`,``,d,`- Check the command, runtime, and working directory, then retry once.`,`- Use a read-only diagnostic to confirm the missing dependency or environment value.`,`- Ask the user if recovery requires changing the requested approach.`].join(`
|
|
1
|
+
import{getBackgroundThresholdMs as e}from"../../types/config.mjs";import{stripAnsi as t}from"../../services/AnsiScrub/ansiScrub.mjs";import{formatSize as n,summarizeLog as r,textResult as i}from"./responseEnvelope.mjs";import{BASH_TOOL_LABEL as a,BASH_TOOL_NAME as o,BashParamsSchema as s}from"../../schemas/bashTool.mjs";import{renderBashCall as c,renderBashResult as l}from"../../tui/bashRender.mjs";const u=1e3,d=`Options:`,f={requested:`Started in the background`,threshold:`Still running after the background threshold, so it was moved to the background`,interactive:`Started on a terminal so it can prompt, which means it runs in the background`},p=`Execute bash commands, with long-running ones supervised in the background`;function m(t=e()){return[`A command still running after ${Math.round(t/u)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,`Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.`,`Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.`,`Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.`,`Stop runners you no longer need. Every runner is stopped automatically when the session ends.`]}function h(e,t){e.registerTool({name:o,label:a,description:`Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.`,promptSnippet:p,promptGuidelines:m(),parameters:s,renderShell:`self`,async execute(e,n,r,a,o){let{command:s,timeout:c,background:l,interactive:f,name:p}=n,m;a&&a(i(`Starting ${f===!0?`interactive runner`:l===!0?`background runner`:`command`}...`)),l!==!0&&f!==!0&&a&&(m=e=>a(i(e)));let h;try{h=await t.bashRunService.run({command:s,timeoutMs:c===void 0?void 0:c*u,background:l,interactive:f,name:p,...m?{onOutput:m}:{},sessionId:await t.getSessionId()})}catch(e){let t=e instanceof Error?e.message:String(e);throw Error([`Could not execute the command: ${t}`,``,d,`- Check the command, runtime, and working directory, then retry once.`,`- Use a read-only diagnostic to confirm the missing dependency or environment value.`,`- Ask the user if recovery requires changing the requested approach.`].join(`
|
|
2
2
|
`),{cause:e})}return h.kind===`promoted`&&t.onRunnerStarted(h.id),g(h)},renderCall(e,t,n){return c(e,t)},renderResult(e,t,n,r){return l(e,{...t,isError:r.isError},n)}})}function g(e){if(e.kind===`failed`)throw Error([`Could not start runner "${e.name}": ${e.error}`,``,d,`- Correct the command or missing executable, then retry once.`,`- Use a read-only diagnostic to inspect the runtime environment.`,`- Ask the user if a different execution approach is required.`].join(`
|
|
3
3
|
`));if(e.kind===`promoted`){let t=[`${f[e.reason]} as runner "${e.name}".`,`Runner ID: ${e.id}`,`Backend: ${e.backend}`,`Log (streaming): ${e.logPath}`,`Inspect it with: doom-runner logs ${e.id}`].filter(e=>e.length>0).join(`
|
|
4
4
|
`);return{...i(t,{id:e.id,runner:e.name,pid:e.pid,logPath:e.logPath,promoted:!0,reason:e.reason})}}let a=r(e.logPath),o=[];e.timedOut?o.push(`Command stopped: it exceeded the requested timeout.`):e.exitCode===null?e.signal!==null&&o.push(`Terminated by ${e.signal}`):o.push(`Exit: completed with code ${e.exitCode}.`);let s=t(a.tail),c=[s.length>0?`Log tail (${a.tailLines} lines):\n${s}`:`Log tail: (no output)`,`Runner ID: ${e.id}`,...o,`Log: ${e.logPath}`,`File size: ${n(a.bytes)}`,`Lines: ${a.lines.toLocaleString(`en-US`)}`].join(`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashTool.mjs","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/ansiScrub';\nimport type { BashRunResult, IBashRunService } from '../../types/bashRunService';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport { getBackgroundThresholdMs } from '../../types/config.ts';\nimport { formatSize, summarizeLog, type ToolResult, textResult } from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\nconst ERROR_OPTIONS_HEADING = 'Options:';\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold, so it was moved to the background',\n interactive: 'Started on a terminal so it can prompt, which means it runs in the background',\n};\n\nexport const BASH_PROMPT_SNIPPET = 'Execute bash commands, with long-running ones supervised in the background';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,\n 'Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.',\n 'Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.',\n 'Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.',\n 'Stop runners you no longer need. Every runner is stopped automatically when the session ends.',\n ];\n}\n\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, _signal, onUpdate, _ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n sessionId: dependencies.getSessionId(),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute the command: ${message}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Check the command, runtime, and working directory, then retry once.',\n '- Use a read-only diagnostic to confirm the missing dependency or environment value.',\n '- Ask the user if recovery requires changing the requested approach.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result);\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, context) {\n return renderBashResult(result, { ...options, isError: context.isError }, theme);\n },\n });\n}\n\nexport function formatRunResult(result: BashRunResult): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Correct the command or missing executable, then retry once.',\n '- Use a read-only diagnostic to inspect the runtime environment.',\n '- Ask the user if a different execution approach is required.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason} as runner \"${result.name}\".`,\n `Runner ID: ${result.id}`,\n `Backend: ${result.backend}`,\n `Log (streaming): ${result.logPath}`,\n `Inspect it with: doom-runner logs ${result.id}`,\n ]\n .filter((line) => line.length > 0)\n .join('\\n');\n\n return {\n ...textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n }),\n };\n }\n\n const log = summarizeLog(result.logPath);\n const notes: string[] = [];\n if (result.timedOut) notes.push('Command stopped: it exceeded the requested timeout.');\n else if (result.exitCode !== null) notes.push(`Exit: completed with code ${result.exitCode}.`);\n else if (result.signal !== null) notes.push(`Terminated by ${result.signal}`);\n\n // The model reads plain text; only the renderer gets the coloured copy below.\n const plainTail = stripAnsi(log.tail);\n const text = [\n plainTail.length > 0 ? `Log tail (${log.tailLines} lines):\\n${plainTail}` : 'Log tail: (no output)',\n `Runner ID: ${result.id}`,\n ...notes,\n `Log: ${result.logPath}`,\n `File size: ${formatSize(log.bytes)}`,\n `Lines: ${log.lines.toLocaleString('en-US')}`,\n ].join('\\n');\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: log.lines,\n // Carried structurally so renderResult shows the output instead of the footer.\n tail: log.tail,\n tailLines: log.tailLines,\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n if (result.timedOut || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0)) {\n throw new Error(\n [\n text,\n '',\n ERROR_OPTIONS_HEADING,\n `- Inspect the full log at ${result.logPath}, correct the cause, then retry once.`,\n '- Run a read-only diagnostic if the output does not explain the failure.',\n '- Ask the user if the failure requires changing the requested approach.',\n ].join('\\n'),\n );\n }\n return textResult(text, details);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bashTool.mjs","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/ansiScrub';\nimport type { BashRunResult, IBashRunService } from '../../types/bashRunService';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport { getBackgroundThresholdMs } from '../../types/config.ts';\nimport { formatSize, summarizeLog, type ToolResult, textResult } from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\nconst ERROR_OPTIONS_HEADING = 'Options:';\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold, so it was moved to the background',\n interactive: 'Started on a terminal so it can prompt, which means it runs in the background',\n};\n\nexport const BASH_PROMPT_SNIPPET = 'Execute bash commands, with long-running ones supervised in the background';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,\n 'Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.',\n 'Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.',\n 'Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.',\n 'Stop runners you no longer need. Every runner is stopped automatically when the session ends.',\n ];\n}\n\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string | Promise<string>;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, _signal, onUpdate, _ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n sessionId: await dependencies.getSessionId(),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute the command: ${message}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Check the command, runtime, and working directory, then retry once.',\n '- Use a read-only diagnostic to confirm the missing dependency or environment value.',\n '- Ask the user if recovery requires changing the requested approach.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result);\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, context) {\n return renderBashResult(result, { ...options, isError: context.isError }, theme);\n },\n });\n}\n\nexport function formatRunResult(result: BashRunResult): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Correct the command or missing executable, then retry once.',\n '- Use a read-only diagnostic to inspect the runtime environment.',\n '- Ask the user if a different execution approach is required.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason} as runner \"${result.name}\".`,\n `Runner ID: ${result.id}`,\n `Backend: ${result.backend}`,\n `Log (streaming): ${result.logPath}`,\n `Inspect it with: doom-runner logs ${result.id}`,\n ]\n .filter((line) => line.length > 0)\n .join('\\n');\n\n return {\n ...textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n }),\n };\n }\n\n const log = summarizeLog(result.logPath);\n const notes: string[] = [];\n if (result.timedOut) notes.push('Command stopped: it exceeded the requested timeout.');\n else if (result.exitCode !== null) notes.push(`Exit: completed with code ${result.exitCode}.`);\n else if (result.signal !== null) notes.push(`Terminated by ${result.signal}`);\n\n // The model reads plain text; only the renderer gets the coloured copy below.\n const plainTail = stripAnsi(log.tail);\n const text = [\n plainTail.length > 0 ? `Log tail (${log.tailLines} lines):\\n${plainTail}` : 'Log tail: (no output)',\n `Runner ID: ${result.id}`,\n ...notes,\n `Log: ${result.logPath}`,\n `File size: ${formatSize(log.bytes)}`,\n `Lines: ${log.lines.toLocaleString('en-US')}`,\n ].join('\\n');\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: log.lines,\n // Carried structurally so renderResult shows the output instead of the footer.\n tail: log.tail,\n tailLines: log.tailLines,\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n if (result.timedOut || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0)) {\n throw new Error(\n [\n text,\n '',\n ERROR_OPTIONS_HEADING,\n `- Inspect the full log at ${result.logPath}, correct the cause, then retry once.`,\n '- Run a read-only diagnostic if the output does not explain the failure.',\n '- Ask the user if the failure requires changing the requested approach.',\n ].join('\\n'),\n );\n }\n return textResult(text, details);\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("node:url");const i={GIT_PAGER:`cat`,PAGER:`cat`,GIT_TERMINAL_PROMPT:`0`},a=`ENOENT`;function o(e,t){return{spec:n.default.join(e,`${t}.command.json`),gate:n.default.join(e,`${t}.gate`),exit:n.default.join(e,`${t}.exit.json`),logDone:n.default.join(e,`${t}.log.done`)}}function s(e,n){t.default.writeFileSync(e,`${JSON.stringify(n)}\n`,{mode:384})}function c(e){try{let n=JSON.parse(t.default.readFileSync(e,`utf8`));if(typeof n!=`object`||!n)return;let r=n;return{code:typeof r.code==`number`?r.code:null,signal:typeof r.signal==`string`?r.signal:null}}catch(t){t.code!==a&&process.emitWarning(`Could not read runner exit metadata ${e}: ${String(t)}`);return}}function l(e){for(let n of Object.values(e))try{t.default.unlinkSync(n)}catch(e){e.code!==a&&process.emitWarning(String(e))}}function u(e
|
|
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("node:url");const i={GIT_PAGER:`cat`,PAGER:`cat`,GIT_TERMINAL_PROMPT:`0`},a=`ENOENT`;function o(e,t){return{spec:n.default.join(e,`${t}.command.json`),gate:n.default.join(e,`${t}.gate`),exit:n.default.join(e,`${t}.exit.json`),logDone:n.default.join(e,`${t}.log.done`)}}function s(e,n){t.default.writeFileSync(e,`${JSON.stringify(n)}\n`,{mode:384})}function c(e){try{let n=JSON.parse(t.default.readFileSync(e,`utf8`));if(typeof n!=`object`||!n)return;let r=n;return{code:typeof r.code==`number`?r.code:null,signal:typeof r.signal==`string`?r.signal:null}}catch(t){t.code!==a&&process.emitWarning(`Could not read runner exit metadata ${e}: ${String(t)}`);return}}function l(e){for(let n of Object.values(e))try{t.default.unlinkSync(n)}catch(e){e.code!==a&&process.emitWarning(String(e))}}function u(e,i=require("url").pathToFileURL(__filename).href){let a=(0,r.fileURLToPath)(i),o=n.default.extname(a)===`.ts`?`ts`:`mjs`,s=n.default.dirname(a);for(;;){let r=n.default.join(s,`bin`,`${e}.${o}`);if(t.default.existsSync(r))return r;let i=n.default.dirname(s);if(i===s)break;s=i}throw Error(`Cannot find bin/${e}.${o} by walking up from ${a}.`)}function d(e){return`exec ${f([process.execPath,u(`runnerHost`),e.spec,e.gate,e.exit])}`}function f(e){return e.map(e=>`'${e.replaceAll(`'`,`'"'"'`)}'`).join(` `)}exports.NO_TERMINAL_INPUT_ENV=i,exports.cleanupSupervisorFiles=l,exports.readExitMetadata=c,exports.runtimeEntry=u,exports.shellJoin=f,exports.supervisorCommand=d,exports.supervisorPaths=o,exports.writeCommandSpec=s;
|
|
2
2
|
//# sourceMappingURL=runnerSpec.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runnerSpec.cjs","names":["path","fs","fileURLToPath"],"sources":["../../../src/schemas/runnerSpec.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/** What a supervisor needs to run one command. */\nexport interface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\n/** Exit status the supervisor records once its command is gone. */\nexport interface ExitMetadata {\n code: number | null;\n signal: NodeJS.Signals | null;\n}\n\n/** Sidecars a supervised run reads and writes beside its log. */\nexport interface SupervisorPaths {\n spec: string;\n gate: string;\n exit: string;\n logDone: string;\n}\n\n/**\n * Settings that make a terminal safe for a caller with no keyboard.\n *\n * Commands run inside an rmux pane, so their stdout is a real tty. Git and\n * friends take that as permission to open a pager, and `less` then waits at\n * `(END)` for a keypress nobody is there to send, so the run hangs until its\n * timeout. The same tty invites credential prompts, which hang the same way.\n * A conventional piped stdout disables both automatically; a pane has to be\n * told. A command that genuinely wants a pager can still ask for one, as in\n * `git -c core.pager=less log`.\n */\nexport const NO_TERMINAL_INPUT_ENV = {\n GIT_PAGER: 'cat',\n PAGER: 'cat',\n GIT_TERMINAL_PROMPT: '0',\n};\n\nconst SPEC_MODE = 0o600;\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\n\nexport function supervisorPaths(directory: string, id: string): SupervisorPaths {\n return {\n spec: path.join(directory, `${id}.command.json`),\n gate: path.join(directory, `${id}.gate`),\n exit: path.join(directory, `${id}.exit.json`),\n logDone: path.join(directory, `${id}.log.done`),\n };\n}\n\nexport function writeCommandSpec(target: string, spec: CommandSpec): void {\n fs.writeFileSync(target, `${JSON.stringify(spec)}\\n`, { mode: SPEC_MODE });\n}\n\nexport function readExitMetadata(target: string): ExitMetadata | undefined {\n try {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) return undefined;\n const metadata = value as Partial<ExitMetadata>;\n const code = typeof metadata.code === 'number' ? metadata.code : null;\n const signal = typeof metadata.signal === 'string' ? (metadata.signal as NodeJS.Signals) : null;\n return { code, signal };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n process.emitWarning(`Could not read runner exit metadata ${target}: ${String(error)}`);\n }\n return undefined;\n }\n}\n\nexport function cleanupSupervisorFiles(paths: SupervisorPaths): void {\n for (const target of Object.values(paths)) {\n try {\n fs.unlinkSync(target);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) process.emitWarning(String(error));\n }\n }\n}\n\n
|
|
1
|
+
{"version":3,"file":"runnerSpec.cjs","names":["path","fs","fileURLToPath"],"sources":["../../../src/schemas/runnerSpec.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/** What a supervisor needs to run one command. */\nexport interface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\n/** Exit status the supervisor records once its command is gone. */\nexport interface ExitMetadata {\n code: number | null;\n signal: NodeJS.Signals | null;\n}\n\n/** Sidecars a supervised run reads and writes beside its log. */\nexport interface SupervisorPaths {\n spec: string;\n gate: string;\n exit: string;\n logDone: string;\n}\n\n/**\n * Settings that make a terminal safe for a caller with no keyboard.\n *\n * Commands run inside an rmux pane, so their stdout is a real tty. Git and\n * friends take that as permission to open a pager, and `less` then waits at\n * `(END)` for a keypress nobody is there to send, so the run hangs until its\n * timeout. The same tty invites credential prompts, which hang the same way.\n * A conventional piped stdout disables both automatically; a pane has to be\n * told. A command that genuinely wants a pager can still ask for one, as in\n * `git -c core.pager=less log`.\n */\nexport const NO_TERMINAL_INPUT_ENV = {\n GIT_PAGER: 'cat',\n PAGER: 'cat',\n GIT_TERMINAL_PROMPT: '0',\n};\n\nconst SPEC_MODE = 0o600;\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\n\nexport function supervisorPaths(directory: string, id: string): SupervisorPaths {\n return {\n spec: path.join(directory, `${id}.command.json`),\n gate: path.join(directory, `${id}.gate`),\n exit: path.join(directory, `${id}.exit.json`),\n logDone: path.join(directory, `${id}.log.done`),\n };\n}\n\nexport function writeCommandSpec(target: string, spec: CommandSpec): void {\n fs.writeFileSync(target, `${JSON.stringify(spec)}\\n`, { mode: SPEC_MODE });\n}\n\nexport function readExitMetadata(target: string): ExitMetadata | undefined {\n try {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) return undefined;\n const metadata = value as Partial<ExitMetadata>;\n const code = typeof metadata.code === 'number' ? metadata.code : null;\n const signal = typeof metadata.signal === 'string' ? (metadata.signal as NodeJS.Signals) : null;\n return { code, signal };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n process.emitWarning(`Could not read runner exit metadata ${target}: ${String(error)}`);\n }\n return undefined;\n }\n}\n\nexport function cleanupSupervisorFiles(paths: SupervisorPaths): void {\n for (const target of Object.values(paths)) {\n try {\n fs.unlinkSync(target);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) process.emitWarning(String(error));\n }\n }\n}\n\n/**\n * Resolves an executable entry point by walking up to the `bin` directory that\n * ships beside this module. Source and published layouts nest `schemas` at\n * different depths, and a hardcoded depth resolves to a path that stays silent\n * until the shell tries to exec it.\n */\nexport function runtimeEntry(name: 'runnerHost' | 'logSink', moduleUrl = import.meta.url): string {\n const modulePath = fileURLToPath(moduleUrl);\n const extension = path.extname(modulePath) === '.ts' ? 'ts' : 'mjs';\n let directory = path.dirname(modulePath);\n\n while (true) {\n const entry = path.join(directory, 'bin', `${name}.${extension}`);\n if (fs.existsSync(entry)) return entry;\n\n const parent = path.dirname(directory);\n if (parent === directory) break;\n directory = parent;\n }\n\n throw new Error(`Cannot find bin/${name}.${extension} by walking up from ${modulePath}.`);\n}\n\n/**\n * Replaces the shell rather than nesting under it, so the pid handed back is the\n * supervisor's and a group signal reaches it directly.\n */\nexport function supervisorCommand(paths: SupervisorPaths): string {\n return `exec ${shellJoin([process.execPath, runtimeEntry('runnerHost'), paths.spec, paths.gate, paths.exit])}`;\n}\n\nexport function shellJoin(values: readonly string[]): string {\n return values.map((value) => `'${value.replaceAll(\"'\", `'\"'\"'`)}'`).join(' ');\n}\n"],"mappings":"wKAoCA,MAAa,EAAwB,CACnC,UAAW,MACX,MAAO,MACP,oBAAqB,GACvB,EAGM,EAAuB,SAE7B,SAAgB,EAAgB,EAAmB,EAA6B,CAC9E,MAAO,CACL,KAAMA,EAAAA,QAAK,KAAK,EAAW,GAAG,EAAG,cAAc,EAC/C,KAAMA,EAAAA,QAAK,KAAK,EAAW,GAAG,EAAG,MAAM,EACvC,KAAMA,EAAAA,QAAK,KAAK,EAAW,GAAG,EAAG,WAAW,EAC5C,QAASA,EAAAA,QAAK,KAAK,EAAW,GAAG,EAAG,UAAU,CAChD,CACF,CAEA,SAAgB,EAAiB,EAAgB,EAAyB,CACxE,EAAA,QAAG,cAAc,EAAQ,GAAG,KAAK,UAAU,CAAI,EAAE,IAAK,CAAE,KAAM,GAAU,CAAC,CAC3E,CAEA,SAAgB,EAAiB,EAA0C,CACzE,GAAI,CACF,IAAM,EAAiB,KAAK,MAAMC,EAAAA,QAAG,aAAa,EAAQ,MAAM,CAAC,EACjE,GAAI,OAAO,GAAU,WAAY,EAAgB,OACjD,IAAM,EAAW,EAGjB,MAAO,CAAE,KAFI,OAAO,EAAS,MAAS,SAAW,EAAS,KAAO,KAElD,OADA,OAAO,EAAS,QAAW,SAAY,EAAS,OAA4B,IACrE,CACxB,OAAS,EAAO,CACT,EAAgC,OAAS,GAC5C,QAAQ,YAAY,uCAAuC,EAAO,IAAI,OAAO,CAAK,GAAG,EAEvF,MACF,CACF,CAEA,SAAgB,EAAuB,EAA8B,CACnE,IAAK,IAAM,KAAU,OAAO,OAAO,CAAK,EACtC,GAAI,CACF,EAAA,QAAG,WAAW,CAAM,CACtB,OAAS,EAAO,CACT,EAAgC,OAAS,GAAsB,QAAQ,YAAY,OAAO,CAAK,CAAC,CACvG,CAEJ,CAQA,SAAgB,EAAa,EAAgC,EAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,KAAqC,CAChG,IAAM,GAAA,EAAaC,EAAAA,cAAAA,CAAc,CAAS,EACpC,EAAYF,EAAAA,QAAK,QAAQ,CAAU,IAAM,MAAQ,KAAO,MAC1D,EAAYA,EAAAA,QAAK,QAAQ,CAAU,EAEvC,OAAa,CACX,IAAM,EAAQA,EAAAA,QAAK,KAAK,EAAW,MAAO,GAAG,EAAK,GAAG,GAAW,EAChE,GAAIC,EAAAA,QAAG,WAAW,CAAK,EAAG,OAAO,EAEjC,IAAM,EAASD,EAAAA,QAAK,QAAQ,CAAS,EACrC,GAAI,IAAW,EAAW,MAC1B,EAAY,CACd,CAEA,MAAU,MAAM,mBAAmB,EAAK,GAAG,EAAU,sBAAsB,EAAW,EAAE,CAC1F,CAMA,SAAgB,EAAkB,EAAgC,CAChE,MAAO,QAAQ,EAAU,CAAC,QAAQ,SAAU,EAAa,YAAY,EAAG,EAAM,KAAM,EAAM,KAAM,EAAM,IAAI,CAAC,GAC7G,CAEA,SAAgB,EAAU,EAAmC,CAC3D,OAAO,EAAO,IAAK,GAAU,IAAI,EAAM,WAAW,IAAK,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAC9E"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";import{fileURLToPath as n}from"node:url";const r={GIT_PAGER:`cat`,PAGER:`cat`,GIT_TERMINAL_PROMPT:`0`},i=`ENOENT`;function a(e,n){return{spec:t.join(e,`${n}.command.json`),gate:t.join(e,`${n}.gate`),exit:t.join(e,`${n}.exit.json`),logDone:t.join(e,`${n}.log.done`)}}function o(t,n){e.writeFileSync(t,`${JSON.stringify(n)}\n`,{mode:384})}function s(t){try{let n=JSON.parse(e.readFileSync(t,`utf8`));if(typeof n!=`object`||!n)return;let r=n;return{code:typeof r.code==`number`?r.code:null,signal:typeof r.signal==`string`?r.signal:null}}catch(e){e.code!==i&&process.emitWarning(`Could not read runner exit metadata ${t}: ${String(e)}`);return}}function c(t){for(let n of Object.values(t))try{e.unlinkSync(n)}catch(e){e.code!==i&&process.emitWarning(String(e))}}function l(
|
|
1
|
+
import e from"node:fs";import t from"node:path";import{fileURLToPath as n}from"node:url";const r={GIT_PAGER:`cat`,PAGER:`cat`,GIT_TERMINAL_PROMPT:`0`},i=`ENOENT`;function a(e,n){return{spec:t.join(e,`${n}.command.json`),gate:t.join(e,`${n}.gate`),exit:t.join(e,`${n}.exit.json`),logDone:t.join(e,`${n}.log.done`)}}function o(t,n){e.writeFileSync(t,`${JSON.stringify(n)}\n`,{mode:384})}function s(t){try{let n=JSON.parse(e.readFileSync(t,`utf8`));if(typeof n!=`object`||!n)return;let r=n;return{code:typeof r.code==`number`?r.code:null,signal:typeof r.signal==`string`?r.signal:null}}catch(e){e.code!==i&&process.emitWarning(`Could not read runner exit metadata ${t}: ${String(e)}`);return}}function c(t){for(let n of Object.values(t))try{e.unlinkSync(n)}catch(e){e.code!==i&&process.emitWarning(String(e))}}function l(r,i=import.meta.url){let a=n(i),o=t.extname(a)===`.ts`?`ts`:`mjs`,s=t.dirname(a);for(;;){let n=t.join(s,`bin`,`${r}.${o}`);if(e.existsSync(n))return n;let i=t.dirname(s);if(i===s)break;s=i}throw Error(`Cannot find bin/${r}.${o} by walking up from ${a}.`)}function u(e){return`exec ${d([process.execPath,l(`runnerHost`),e.spec,e.gate,e.exit])}`}function d(e){return e.map(e=>`'${e.replaceAll(`'`,`'"'"'`)}'`).join(` `)}export{r as NO_TERMINAL_INPUT_ENV,c as cleanupSupervisorFiles,s as readExitMetadata,l as runtimeEntry,d as shellJoin,u as supervisorCommand,a as supervisorPaths,o as writeCommandSpec};
|
|
2
2
|
//# sourceMappingURL=runnerSpec.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runnerSpec.mjs","names":[],"sources":["../../../src/schemas/runnerSpec.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/** What a supervisor needs to run one command. */\nexport interface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\n/** Exit status the supervisor records once its command is gone. */\nexport interface ExitMetadata {\n code: number | null;\n signal: NodeJS.Signals | null;\n}\n\n/** Sidecars a supervised run reads and writes beside its log. */\nexport interface SupervisorPaths {\n spec: string;\n gate: string;\n exit: string;\n logDone: string;\n}\n\n/**\n * Settings that make a terminal safe for a caller with no keyboard.\n *\n * Commands run inside an rmux pane, so their stdout is a real tty. Git and\n * friends take that as permission to open a pager, and `less` then waits at\n * `(END)` for a keypress nobody is there to send, so the run hangs until its\n * timeout. The same tty invites credential prompts, which hang the same way.\n * A conventional piped stdout disables both automatically; a pane has to be\n * told. A command that genuinely wants a pager can still ask for one, as in\n * `git -c core.pager=less log`.\n */\nexport const NO_TERMINAL_INPUT_ENV = {\n GIT_PAGER: 'cat',\n PAGER: 'cat',\n GIT_TERMINAL_PROMPT: '0',\n};\n\nconst SPEC_MODE = 0o600;\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\n\nexport function supervisorPaths(directory: string, id: string): SupervisorPaths {\n return {\n spec: path.join(directory, `${id}.command.json`),\n gate: path.join(directory, `${id}.gate`),\n exit: path.join(directory, `${id}.exit.json`),\n logDone: path.join(directory, `${id}.log.done`),\n };\n}\n\nexport function writeCommandSpec(target: string, spec: CommandSpec): void {\n fs.writeFileSync(target, `${JSON.stringify(spec)}\\n`, { mode: SPEC_MODE });\n}\n\nexport function readExitMetadata(target: string): ExitMetadata | undefined {\n try {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) return undefined;\n const metadata = value as Partial<ExitMetadata>;\n const code = typeof metadata.code === 'number' ? metadata.code : null;\n const signal = typeof metadata.signal === 'string' ? (metadata.signal as NodeJS.Signals) : null;\n return { code, signal };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n process.emitWarning(`Could not read runner exit metadata ${target}: ${String(error)}`);\n }\n return undefined;\n }\n}\n\nexport function cleanupSupervisorFiles(paths: SupervisorPaths): void {\n for (const target of Object.values(paths)) {\n try {\n fs.unlinkSync(target);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) process.emitWarning(String(error));\n }\n }\n}\n\n
|
|
1
|
+
{"version":3,"file":"runnerSpec.mjs","names":[],"sources":["../../../src/schemas/runnerSpec.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/** What a supervisor needs to run one command. */\nexport interface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\n/** Exit status the supervisor records once its command is gone. */\nexport interface ExitMetadata {\n code: number | null;\n signal: NodeJS.Signals | null;\n}\n\n/** Sidecars a supervised run reads and writes beside its log. */\nexport interface SupervisorPaths {\n spec: string;\n gate: string;\n exit: string;\n logDone: string;\n}\n\n/**\n * Settings that make a terminal safe for a caller with no keyboard.\n *\n * Commands run inside an rmux pane, so their stdout is a real tty. Git and\n * friends take that as permission to open a pager, and `less` then waits at\n * `(END)` for a keypress nobody is there to send, so the run hangs until its\n * timeout. The same tty invites credential prompts, which hang the same way.\n * A conventional piped stdout disables both automatically; a pane has to be\n * told. A command that genuinely wants a pager can still ask for one, as in\n * `git -c core.pager=less log`.\n */\nexport const NO_TERMINAL_INPUT_ENV = {\n GIT_PAGER: 'cat',\n PAGER: 'cat',\n GIT_TERMINAL_PROMPT: '0',\n};\n\nconst SPEC_MODE = 0o600;\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\n\nexport function supervisorPaths(directory: string, id: string): SupervisorPaths {\n return {\n spec: path.join(directory, `${id}.command.json`),\n gate: path.join(directory, `${id}.gate`),\n exit: path.join(directory, `${id}.exit.json`),\n logDone: path.join(directory, `${id}.log.done`),\n };\n}\n\nexport function writeCommandSpec(target: string, spec: CommandSpec): void {\n fs.writeFileSync(target, `${JSON.stringify(spec)}\\n`, { mode: SPEC_MODE });\n}\n\nexport function readExitMetadata(target: string): ExitMetadata | undefined {\n try {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) return undefined;\n const metadata = value as Partial<ExitMetadata>;\n const code = typeof metadata.code === 'number' ? metadata.code : null;\n const signal = typeof metadata.signal === 'string' ? (metadata.signal as NodeJS.Signals) : null;\n return { code, signal };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) {\n process.emitWarning(`Could not read runner exit metadata ${target}: ${String(error)}`);\n }\n return undefined;\n }\n}\n\nexport function cleanupSupervisorFiles(paths: SupervisorPaths): void {\n for (const target of Object.values(paths)) {\n try {\n fs.unlinkSync(target);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== NOT_FOUND_ERROR_CODE) process.emitWarning(String(error));\n }\n }\n}\n\n/**\n * Resolves an executable entry point by walking up to the `bin` directory that\n * ships beside this module. Source and published layouts nest `schemas` at\n * different depths, and a hardcoded depth resolves to a path that stays silent\n * until the shell tries to exec it.\n */\nexport function runtimeEntry(name: 'runnerHost' | 'logSink', moduleUrl = import.meta.url): string {\n const modulePath = fileURLToPath(moduleUrl);\n const extension = path.extname(modulePath) === '.ts' ? 'ts' : 'mjs';\n let directory = path.dirname(modulePath);\n\n while (true) {\n const entry = path.join(directory, 'bin', `${name}.${extension}`);\n if (fs.existsSync(entry)) return entry;\n\n const parent = path.dirname(directory);\n if (parent === directory) break;\n directory = parent;\n }\n\n throw new Error(`Cannot find bin/${name}.${extension} by walking up from ${modulePath}.`);\n}\n\n/**\n * Replaces the shell rather than nesting under it, so the pid handed back is the\n * supervisor's and a group signal reaches it directly.\n */\nexport function supervisorCommand(paths: SupervisorPaths): string {\n return `exec ${shellJoin([process.execPath, runtimeEntry('runnerHost'), paths.spec, paths.gate, paths.exit])}`;\n}\n\nexport function shellJoin(values: readonly string[]): string {\n return values.map((value) => `'${value.replaceAll(\"'\", `'\"'\"'`)}'`).join(' ');\n}\n"],"mappings":"yFAoCA,MAAa,EAAwB,CACnC,UAAW,MACX,MAAO,MACP,oBAAqB,GACvB,EAGM,EAAuB,SAE7B,SAAgB,EAAgB,EAAmB,EAA6B,CAC9E,MAAO,CACL,KAAM,EAAK,KAAK,EAAW,GAAG,EAAG,cAAc,EAC/C,KAAM,EAAK,KAAK,EAAW,GAAG,EAAG,MAAM,EACvC,KAAM,EAAK,KAAK,EAAW,GAAG,EAAG,WAAW,EAC5C,QAAS,EAAK,KAAK,EAAW,GAAG,EAAG,UAAU,CAChD,CACF,CAEA,SAAgB,EAAiB,EAAgB,EAAyB,CACxE,EAAG,cAAc,EAAQ,GAAG,KAAK,UAAU,CAAI,EAAE,IAAK,CAAE,KAAM,GAAU,CAAC,CAC3E,CAEA,SAAgB,EAAiB,EAA0C,CACzE,GAAI,CACF,IAAM,EAAiB,KAAK,MAAM,EAAG,aAAa,EAAQ,MAAM,CAAC,EACjE,GAAI,OAAO,GAAU,WAAY,EAAgB,OACjD,IAAM,EAAW,EAGjB,MAAO,CAAE,KAFI,OAAO,EAAS,MAAS,SAAW,EAAS,KAAO,KAElD,OADA,OAAO,EAAS,QAAW,SAAY,EAAS,OAA4B,IACrE,CACxB,OAAS,EAAO,CACT,EAAgC,OAAS,GAC5C,QAAQ,YAAY,uCAAuC,EAAO,IAAI,OAAO,CAAK,GAAG,EAEvF,MACF,CACF,CAEA,SAAgB,EAAuB,EAA8B,CACnE,IAAK,IAAM,KAAU,OAAO,OAAO,CAAK,EACtC,GAAI,CACF,EAAG,WAAW,CAAM,CACtB,OAAS,EAAO,CACT,EAAgC,OAAS,GAAsB,QAAQ,YAAY,OAAO,CAAK,CAAC,CACvG,CAEJ,CAQA,SAAgB,EAAa,EAAgC,EAAY,YAAY,IAAa,CAChG,IAAM,EAAa,EAAc,CAAS,EACpC,EAAY,EAAK,QAAQ,CAAU,IAAM,MAAQ,KAAO,MAC1D,EAAY,EAAK,QAAQ,CAAU,EAEvC,OAAa,CACX,IAAM,EAAQ,EAAK,KAAK,EAAW,MAAO,GAAG,EAAK,GAAG,GAAW,EAChE,GAAI,EAAG,WAAW,CAAK,EAAG,OAAO,EAEjC,IAAM,EAAS,EAAK,QAAQ,CAAS,EACrC,GAAI,IAAW,EAAW,MAC1B,EAAY,CACd,CAEA,MAAU,MAAM,mBAAmB,EAAK,GAAG,EAAU,sBAAsB,EAAW,EAAE,CAC1F,CAMA,SAAgB,EAAkB,EAAgC,CAChE,MAAO,QAAQ,EAAU,CAAC,QAAQ,SAAU,EAAa,YAAY,EAAG,EAAM,KAAM,EAAM,KAAM,EAAM,IAAI,CAAC,GAC7G,CAEA,SAAgB,EAAU,EAAmC,CAC3D,OAAO,EAAO,IAAK,GAAU,IAAI,EAAM,WAAW,IAAK,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAC9E"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
const e=`Active background runners survived compaction:`;function t(t){if(t.length===0)return``;let n=t.flatMap(e=>[`- ${e.name} (${e.id}), log: ${e.logPath}`,` Check: doom-runner status ${e.id}`,` Stream: doom-runner logs ${e.id} --follow`]);return[e,...n,`Use these CLI commands to inspect existing runners before launching another command.`].join(`
|
|
2
2
|
`)}function n(n,r){return r.length===0||n.includes(e)?n:[n,``,t(r)].join(`
|
|
3
|
-
`)}function r(e,n){e.on(`session_compact`,async()=>{let r=n.getSessionId();if(!r)return;let i;try{i=await n.listBySession(r)}catch{return}let a=t(i);a&&e.sendMessage({customType:`doom-runner-compaction`,content:a,display:!1,details:{sessionId:r,runnerIds:i.map(e=>e.id)}},{triggerTurn:!1,deliverAs:`steer`})})}exports.activeRunnerRecovery=t,exports.appendActiveRunnersToSummary=n,exports.registerRunnerCompactionRecovery=r;
|
|
3
|
+
`)}function r(e,n){e.on(`session_compact`,async()=>{let r=await n.getSessionId();if(!r)return;let i;try{i=await n.listBySession(r)}catch{return}let a=t(i);a&&e.sendMessage({customType:`doom-runner-compaction`,content:a,display:!1,details:{sessionId:r,runnerIds:i.map(e=>e.id)}},{triggerTurn:!1,deliverAs:`steer`})})}exports.activeRunnerRecovery=t,exports.appendActiveRunnersToSummary=n,exports.registerRunnerCompactionRecovery=r;
|
|
4
4
|
//# sourceMappingURL=compaction.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.cjs","names":[],"sources":["../../../../src/services/runs/compaction.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\n\nconst COMPACTION_RUNNERS_MARKER = 'Active background runners survived compaction:';\nconst RUNNER_COMPACTION_MESSAGE = 'doom-runner-compaction';\n\nexport interface RunnerCompactionDependencies {\n getSessionId(): string | undefined
|
|
1
|
+
{"version":3,"file":"compaction.cjs","names":[],"sources":["../../../../src/services/runs/compaction.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\n\nconst COMPACTION_RUNNERS_MARKER = 'Active background runners survived compaction:';\nconst RUNNER_COMPACTION_MESSAGE = 'doom-runner-compaction';\n\nexport interface RunnerCompactionDependencies {\n getSessionId(): string | undefined | Promise<string | undefined>;\n listBySession(sessionId: string): Promise<RunnerRecord[]>;\n}\n\nexport function activeRunnerRecovery(active: readonly RunnerRecord[]): string {\n if (active.length === 0) return '';\n const runners = active.flatMap((record) => [\n `- ${record.name} (${record.id}), log: ${record.logPath}`,\n ` Check: doom-runner status ${record.id}`,\n ` Stream: doom-runner logs ${record.id} --follow`,\n ]);\n return [\n COMPACTION_RUNNERS_MARKER,\n ...runners,\n 'Use these CLI commands to inspect existing runners before launching another command.',\n ].join('\\n');\n}\n\nexport function appendActiveRunnersToSummary(summary: string, active: readonly RunnerRecord[]): string {\n if (active.length === 0 || summary.includes(COMPACTION_RUNNERS_MARKER)) return summary;\n return [summary, '', activeRunnerRecovery(active)].join('\\n');\n}\n\nexport function registerRunnerCompactionRecovery(pi: ExtensionAPI, dependencies: RunnerCompactionDependencies): void {\n pi.on('session_compact', async () => {\n const sessionId = await dependencies.getSessionId();\n if (!sessionId) return;\n let active: RunnerRecord[];\n try {\n active = await dependencies.listBySession(sessionId);\n } catch {\n // Recovery is advisory. Omit stale runner hints rather than failing a completed compaction.\n return;\n }\n const content = activeRunnerRecovery(active);\n if (!content) return;\n pi.sendMessage(\n {\n customType: RUNNER_COMPACTION_MESSAGE,\n content,\n display: false,\n details: { sessionId, runnerIds: active.map((record) => record.id) },\n },\n { triggerTurn: false, deliverAs: 'steer' },\n );\n });\n}\n"],"mappings":"AAGA,MAAM,EAA4B,iDAQlC,SAAgB,EAAqB,EAAyC,CAC5E,GAAI,EAAO,SAAW,EAAG,MAAO,GAChC,IAAM,EAAU,EAAO,QAAS,GAAW,CACzC,KAAK,EAAO,KAAK,IAAI,EAAO,GAAG,UAAU,EAAO,UAChD,+BAA+B,EAAO,KACtC,8BAA8B,EAAO,GAAG,UAC1C,CAAC,EACD,MAAO,CACL,EACA,GAAG,EACH,sFACF,CAAC,CAAC,KAAK;CAAI,CACb,CAEA,SAAgB,EAA6B,EAAiB,EAAyC,CAErG,OADI,EAAO,SAAW,GAAK,EAAQ,SAAS,CAAyB,EAAU,EACxE,CAAC,EAAS,GAAI,EAAqB,CAAM,CAAC,CAAC,CAAC,KAAK;CAAI,CAC9D,CAEA,SAAgB,EAAiC,EAAkB,EAAkD,CACnH,EAAG,GAAG,kBAAmB,SAAY,CACnC,IAAM,EAAY,MAAM,EAAa,aAAa,EAClD,GAAI,CAAC,EAAW,OAChB,IAAI,EACJ,GAAI,CACF,EAAS,MAAM,EAAa,cAAc,CAAS,CACrD,MAAQ,CAEN,MACF,CACA,IAAM,EAAU,EAAqB,CAAM,EACtC,GACL,EAAG,YACD,CACE,WAAY,yBACZ,UACA,QAAS,GACT,QAAS,CAAE,YAAW,UAAW,EAAO,IAAK,GAAW,EAAO,EAAE,CAAE,CACrE,EACA,CAAE,YAAa,GAAO,UAAW,OAAQ,CAC3C,CACF,CAAC,CACH"}
|
|
@@ -2,7 +2,7 @@ import { RunnerRecord } from "../../types/runnerRegistry.cjs";
|
|
|
2
2
|
import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
//#region src/services/runs/compaction.d.ts
|
|
4
4
|
interface RunnerCompactionDependencies {
|
|
5
|
-
getSessionId(): string | undefined
|
|
5
|
+
getSessionId(): string | undefined | Promise<string | undefined>;
|
|
6
6
|
listBySession(sessionId: string): Promise<RunnerRecord[]>;
|
|
7
7
|
}
|
|
8
8
|
declare function activeRunnerRecovery(active: readonly RunnerRecord[]): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.d.cts","names":[],"sources":["../../../../src/services/runs/compaction.ts"],"mappings":";;;UAMiB;EACf;
|
|
1
|
+
{"version":3,"file":"compaction.d.cts","names":[],"sources":["../../../../src/services/runs/compaction.ts"],"mappings":";;;UAMiB;EACf,qCAAqC;EACrC,cAAc,oBAAoB,QAAQ;;iBAG5B,qBAAqB,iBAAiB;iBActC,6BAA6B,iBAAiB,iBAAiB;iBAK/D,iCAAiC,IAAI,cAAc,cAAc"}
|
|
@@ -2,7 +2,7 @@ import { RunnerRecord } from "../../types/runnerRegistry.mjs";
|
|
|
2
2
|
import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
//#region src/services/runs/compaction.d.ts
|
|
4
4
|
interface RunnerCompactionDependencies {
|
|
5
|
-
getSessionId(): string | undefined
|
|
5
|
+
getSessionId(): string | undefined | Promise<string | undefined>;
|
|
6
6
|
listBySession(sessionId: string): Promise<RunnerRecord[]>;
|
|
7
7
|
}
|
|
8
8
|
declare function activeRunnerRecovery(active: readonly RunnerRecord[]): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.d.mts","names":[],"sources":["../../../../src/services/runs/compaction.ts"],"mappings":";;;UAMiB;EACf;
|
|
1
|
+
{"version":3,"file":"compaction.d.mts","names":[],"sources":["../../../../src/services/runs/compaction.ts"],"mappings":";;;UAMiB;EACf,qCAAqC;EACrC,cAAc,oBAAoB,QAAQ;;iBAG5B,qBAAqB,iBAAiB;iBActC,6BAA6B,iBAAiB,iBAAiB;iBAK/D,iCAAiC,IAAI,cAAc,cAAc"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
const e=`Active background runners survived compaction:`;function t(t){if(t.length===0)return``;let n=t.flatMap(e=>[`- ${e.name} (${e.id}), log: ${e.logPath}`,` Check: doom-runner status ${e.id}`,` Stream: doom-runner logs ${e.id} --follow`]);return[e,...n,`Use these CLI commands to inspect existing runners before launching another command.`].join(`
|
|
2
2
|
`)}function n(n,r){return r.length===0||n.includes(e)?n:[n,``,t(r)].join(`
|
|
3
|
-
`)}function r(e,n){e.on(`session_compact`,async()=>{let r=n.getSessionId();if(!r)return;let i;try{i=await n.listBySession(r)}catch{return}let a=t(i);a&&e.sendMessage({customType:`doom-runner-compaction`,content:a,display:!1,details:{sessionId:r,runnerIds:i.map(e=>e.id)}},{triggerTurn:!1,deliverAs:`steer`})})}export{t as activeRunnerRecovery,n as appendActiveRunnersToSummary,r as registerRunnerCompactionRecovery};
|
|
3
|
+
`)}function r(e,n){e.on(`session_compact`,async()=>{let r=await n.getSessionId();if(!r)return;let i;try{i=await n.listBySession(r)}catch{return}let a=t(i);a&&e.sendMessage({customType:`doom-runner-compaction`,content:a,display:!1,details:{sessionId:r,runnerIds:i.map(e=>e.id)}},{triggerTurn:!1,deliverAs:`steer`})})}export{t as activeRunnerRecovery,n as appendActiveRunnersToSummary,r as registerRunnerCompactionRecovery};
|
|
4
4
|
//# sourceMappingURL=compaction.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.mjs","names":[],"sources":["../../../../src/services/runs/compaction.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\n\nconst COMPACTION_RUNNERS_MARKER = 'Active background runners survived compaction:';\nconst RUNNER_COMPACTION_MESSAGE = 'doom-runner-compaction';\n\nexport interface RunnerCompactionDependencies {\n getSessionId(): string | undefined
|
|
1
|
+
{"version":3,"file":"compaction.mjs","names":[],"sources":["../../../../src/services/runs/compaction.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\n\nconst COMPACTION_RUNNERS_MARKER = 'Active background runners survived compaction:';\nconst RUNNER_COMPACTION_MESSAGE = 'doom-runner-compaction';\n\nexport interface RunnerCompactionDependencies {\n getSessionId(): string | undefined | Promise<string | undefined>;\n listBySession(sessionId: string): Promise<RunnerRecord[]>;\n}\n\nexport function activeRunnerRecovery(active: readonly RunnerRecord[]): string {\n if (active.length === 0) return '';\n const runners = active.flatMap((record) => [\n `- ${record.name} (${record.id}), log: ${record.logPath}`,\n ` Check: doom-runner status ${record.id}`,\n ` Stream: doom-runner logs ${record.id} --follow`,\n ]);\n return [\n COMPACTION_RUNNERS_MARKER,\n ...runners,\n 'Use these CLI commands to inspect existing runners before launching another command.',\n ].join('\\n');\n}\n\nexport function appendActiveRunnersToSummary(summary: string, active: readonly RunnerRecord[]): string {\n if (active.length === 0 || summary.includes(COMPACTION_RUNNERS_MARKER)) return summary;\n return [summary, '', activeRunnerRecovery(active)].join('\\n');\n}\n\nexport function registerRunnerCompactionRecovery(pi: ExtensionAPI, dependencies: RunnerCompactionDependencies): void {\n pi.on('session_compact', async () => {\n const sessionId = await dependencies.getSessionId();\n if (!sessionId) return;\n let active: RunnerRecord[];\n try {\n active = await dependencies.listBySession(sessionId);\n } catch {\n // Recovery is advisory. Omit stale runner hints rather than failing a completed compaction.\n return;\n }\n const content = activeRunnerRecovery(active);\n if (!content) return;\n pi.sendMessage(\n {\n customType: RUNNER_COMPACTION_MESSAGE,\n content,\n display: false,\n details: { sessionId, runnerIds: active.map((record) => record.id) },\n },\n { triggerTurn: false, deliverAs: 'steer' },\n );\n });\n}\n"],"mappings":"AAGA,MAAM,EAA4B,iDAQlC,SAAgB,EAAqB,EAAyC,CAC5E,GAAI,EAAO,SAAW,EAAG,MAAO,GAChC,IAAM,EAAU,EAAO,QAAS,GAAW,CACzC,KAAK,EAAO,KAAK,IAAI,EAAO,GAAG,UAAU,EAAO,UAChD,+BAA+B,EAAO,KACtC,8BAA8B,EAAO,GAAG,UAC1C,CAAC,EACD,MAAO,CACL,EACA,GAAG,EACH,sFACF,CAAC,CAAC,KAAK;CAAI,CACb,CAEA,SAAgB,EAA6B,EAAiB,EAAyC,CAErG,OADI,EAAO,SAAW,GAAK,EAAQ,SAAS,CAAyB,EAAU,EACxE,CAAC,EAAS,GAAI,EAAqB,CAAM,CAAC,CAAC,CAAC,KAAK;CAAI,CAC9D,CAEA,SAAgB,EAAiC,EAAkB,EAAkD,CACnH,EAAG,GAAG,kBAAmB,SAAY,CACnC,IAAM,EAAY,MAAM,EAAa,aAAa,EAClD,GAAI,CAAC,EAAW,OAChB,IAAI,EACJ,GAAI,CACF,EAAS,MAAM,EAAa,cAAc,CAAS,CACrD,MAAQ,CAEN,MACF,CACA,IAAM,EAAU,EAAqB,CAAM,EACtC,GACL,EAAG,YACD,CACE,WAAY,yBACZ,UACA,QAAS,GACT,QAAS,CAAE,YAAW,UAAW,EAAO,IAAK,GAAW,EAAO,EAAE,CAAE,CACrE,EACA,CAAE,YAAa,GAAO,UAAW,OAAQ,CAC3C,CACF,CAAC,CACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-runner",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.26",
|
|
4
4
|
"description": "Supervised shell execution, background process control, and run logs for Pi coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -74,11 +74,6 @@
|
|
|
74
74
|
"import": "./dist/extensions/pi.mjs",
|
|
75
75
|
"require": "./dist/extensions/pi.cjs"
|
|
76
76
|
},
|
|
77
|
-
"./ptyProvider": {
|
|
78
|
-
"types": "./dist/ptyProvider.d.mts",
|
|
79
|
-
"import": "./dist/ptyProvider.mjs",
|
|
80
|
-
"require": "./dist/ptyProvider.cjs"
|
|
81
|
-
},
|
|
82
77
|
"./reconcile": {
|
|
83
78
|
"types": "./dist/reconcile.d.mts",
|
|
84
79
|
"import": "./dist/reconcile.mjs",
|
|
@@ -291,10 +286,10 @@
|
|
|
291
286
|
"@xterm/headless": "6.0.0",
|
|
292
287
|
"node-pty": "1.1.0",
|
|
293
288
|
"typebox": "1.1.38",
|
|
294
|
-
"@agimon-ai/doompi-
|
|
295
|
-
"@agimon-ai/doompi-
|
|
296
|
-
"@agimon-ai/
|
|
297
|
-
"@agimon-ai/
|
|
289
|
+
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.26",
|
|
290
|
+
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.26",
|
|
291
|
+
"@agimon-ai/doompi-ui": "0.0.1-alpha.26",
|
|
292
|
+
"@agimon-ai/foundation-process-registry": "0.29.16"
|
|
298
293
|
},
|
|
299
294
|
"devDependencies": {
|
|
300
295
|
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
@@ -318,10 +313,10 @@
|
|
|
318
313
|
}
|
|
319
314
|
},
|
|
320
315
|
"optionalDependencies": {
|
|
321
|
-
"@agimon-ai/doompi-runner-rmux-darwin-
|
|
322
|
-
"@agimon-ai/doompi-runner-rmux-linux-
|
|
323
|
-
"@agimon-ai/doompi-runner-rmux-linux-
|
|
324
|
-
"@agimon-ai/doompi-runner-rmux-darwin-
|
|
316
|
+
"@agimon-ai/doompi-runner-rmux-darwin-x64": "0.0.1-alpha.26",
|
|
317
|
+
"@agimon-ai/doompi-runner-rmux-linux-x64": "0.0.1-alpha.26",
|
|
318
|
+
"@agimon-ai/doompi-runner-rmux-linux-arm64": "0.0.1-alpha.26",
|
|
319
|
+
"@agimon-ai/doompi-runner-rmux-darwin-arm64": "0.0.1-alpha.26"
|
|
325
320
|
},
|
|
326
321
|
"engines": {
|
|
327
322
|
"node": ">=22.19.0"
|
package/dist/ptyProvider.cjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./src/adapters/pty/ptyProvider.cjs");exports.provideRunnerPty=e.provideRunnerPty;
|
package/dist/ptyProvider.d.cts
DELETED
package/dist/ptyProvider.d.mts
DELETED
package/dist/ptyProvider.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{provideRunnerPty as e}from"./src/adapters/pty/ptyProvider.mjs";export{e as provideRunnerPty};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
let e=require("@agimon-ai/doompi-extension-contracts/runner-pty");const t=`Runner PTY provider is disposed.`;function n(e,t){return`ext:${e}:${t}`.replaceAll(/[^A-Za-z0-9._-]/gu,`-`)}function r(r,i){let a=new Map,o=!1,s=e=>{let t=a.get(e);t&&(a.delete(e),t.dispose())},c=[r.provide(e.RunnerPtyLaunchProtocol,async(c,l)=>{if(o)return{launched:!1,reason:t};let u=i.sessionId();if(!u)return{launched:!1,reason:`No runner session is active.`};let d=n(l.source,c.id);if(a.has(d))return{launched:!1,reason:`A run named ${c.id} is already active.`};try{let n=await i.ptyHost.launch({id:d,name:d,command:c.command,cwd:c.cwd,sessionId:u,...c.cols===void 0?{}:{cols:c.cols},...c.rows===void 0?{}:{rows:c.rows}});if(o){try{await n.stop()}catch(e){process.emitWarning(`Could not stop a stale runner PTY launch: ${String(e)}`)}return{launched:!1,reason:t}}let l=n.onData(t=>{o||r.notify(e.RunnerPtyData,{id:c.id,chunk:t})});return a.set(d,{run:n,dispose:l}),n.completion().then(t=>{o||r.notify(e.RunnerPtyExit,{id:c.id,...typeof t.code==`number`?{code:t.code}:{},...t.signal?{signal:String(t.signal)}:{}})}).catch(()=>{o||r.notify(e.RunnerPtyExit,{id:c.id,code:-1})}).finally(()=>s(d)),{launched:!0}}catch(e){return o||i.onError?.(e,c.id),{launched:!1,reason:o?t:e instanceof Error?e.message:String(e)}}}),r.onNotification(e.RunnerPtyInput,({id:e,text:t},r)=>{a.get(n(r.source,e))?.run.write(`${t}\n`)}),r.onNotification(e.RunnerPtyStop,async({id:e},t)=>{let r=n(t.source,e),i=a.get(r);i&&(s(r),await i.run.stop())})];return()=>{if(!o){o=!0;for(let e of c)e();for(let e of a.values())e.dispose();a.clear()}}}exports.provideRunnerPty=r;
|
|
2
|
-
//# sourceMappingURL=ptyProvider.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ptyProvider.cjs","names":["RunnerPtyLaunchProtocol","RunnerPtyData","RunnerPtyExit","RunnerPtyInput","RunnerPtyStop"],"sources":["../../../../src/adapters/pty/ptyProvider.ts"],"sourcesContent":["/**\n * Lends this package's pseudo terminals to other extensions.\n *\n * doom-runner is the only package that carries `node-pty`, and the only one that\n * knows how to keep a terminal alive for a session and tear it down with one.\n * Rather than have every extension that needs an answerable command grow its own\n * copy of that, they ask over the bus and the terminal stays here.\n *\n * Output is forwarded raw, chunk by chunk, because the caller is usually\n * rendering a tail and needs the partial line a prompt sits on. Reassembling\n * lines is the caller's problem, not the terminal's.\n *\n * AVOID:\n * - Keeping a run past the session that asked for it\n * - Swallowing an exit; a caller waiting on one would hang forever\n */\n\nimport type { ProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport {\n RunnerPtyData,\n RunnerPtyExit,\n RunnerPtyInput,\n RunnerPtyLaunchProtocol,\n RunnerPtyStop,\n} from '@agimon-ai/doompi-extension-contracts/runner-pty';\nimport type { IPtyHost, PtyRun } from '../../types/ptyHost';\n\n/** Namespaced so a borrowed terminal is distinguishable in the runner list. */\nconst RUN_NAME_PREFIX = 'ext';\nconst DISPOSED_REASON = 'Runner PTY provider is disposed.';\n\n/**\n * Collapses a caller and its run id into one safe path segment.\n *\n * The id becomes a log file name, and package-scoped sources carry a `/`, which\n * `path.join` would turn into a directory nobody created.\n */\nfunction runKey(source: string, id: string): string {\n return `${RUN_NAME_PREFIX}:${source}:${id}`.replaceAll(/[^A-Za-z0-9._-]/gu, '-');\n}\n\nexport interface PtyProviderOptions {\n ptyHost: IPtyHost;\n sessionId: () => string | undefined;\n onError?: (error: unknown, id: string) => void;\n}\n\nexport function provideRunnerPty(runtime: ProtocolRuntime, options: PtyProviderOptions): () => void {\n const runs = new Map<string, { run: PtyRun; dispose: () => void }>();\n let disposed = false;\n\n const release = (id: string): void => {\n const entry = runs.get(id);\n if (!entry) return;\n runs.delete(id);\n entry.dispose();\n };\n\n const disposers = [\n runtime.provide(RunnerPtyLaunchProtocol, async (request, identity) => {\n if (disposed) return { launched: false, reason: DISPOSED_REASON };\n const sessionId = options.sessionId();\n if (!sessionId) return { launched: false, reason: 'No runner session is active.' };\n // Keyed by the caller too, so two extensions choosing the same id cannot\n // collide or read each other's output.\n const key = runKey(identity.source, request.id);\n if (runs.has(key)) return { launched: false, reason: `A run named ${request.id} is already active.` };\n try {\n const run = await options.ptyHost.launch({\n id: key,\n // The host keys its runs by name, so this is the unique one rather\n // than the caller's label; two callers naming a run `install` would\n // otherwise clobber each other.\n name: key,\n command: request.command,\n cwd: request.cwd,\n sessionId,\n ...(request.cols === undefined ? {} : { cols: request.cols }),\n ...(request.rows === undefined ? {} : { rows: request.rows }),\n });\n if (disposed) {\n try {\n await run.stop();\n } catch (error) {\n // The provider no longer has a live caller to report to; the owning\n // PTY host performs a second bounded cleanup during root disposal.\n process.emitWarning(`Could not stop a stale runner PTY launch: ${String(error)}`);\n }\n return { launched: false, reason: DISPOSED_REASON };\n }\n const unsubscribe = run.onData((chunk) => {\n if (!disposed) runtime.notify(RunnerPtyData, { id: request.id, chunk });\n });\n runs.set(key, { run, dispose: unsubscribe });\n void run\n .completion()\n .then((result) => {\n if (disposed) return;\n runtime.notify(RunnerPtyExit, {\n id: request.id,\n ...(typeof result.code === 'number' ? { code: result.code } : {}),\n ...(result.signal ? { signal: String(result.signal) } : {}),\n });\n })\n // A caller blocked on the exit would wait forever otherwise, so a\n // failure to observe one is still reported as an exit.\n .catch(() => {\n if (!disposed) runtime.notify(RunnerPtyExit, { id: request.id, code: -1 });\n })\n .finally(() => release(key));\n return { launched: true };\n } catch (error) {\n if (!disposed) options.onError?.(error, request.id);\n return {\n launched: false,\n reason: disposed ? DISPOSED_REASON : error instanceof Error ? error.message : String(error),\n };\n }\n }),\n runtime.onNotification(RunnerPtyInput, ({ id, text }, identity) => {\n runs.get(runKey(identity.source, id))?.run.write(`${text}\\n`);\n }),\n runtime.onNotification(RunnerPtyStop, async ({ id }, identity) => {\n const key = runKey(identity.source, id);\n const entry = runs.get(key);\n if (!entry) return;\n release(key);\n await entry.run.stop();\n }),\n ];\n\n return () => {\n if (disposed) return;\n disposed = true;\n for (const dispose of disposers) dispose();\n for (const entry of runs.values()) entry.dispose();\n runs.clear();\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { IPtyHost } from "../../types/ptyHost.cjs";
|
|
2
|
-
import { ProtocolRuntime } from "@agimon-ai/doompi-extension-contracts/protocol";
|
|
3
|
-
//#region src/adapters/pty/ptyProvider.d.ts
|
|
4
|
-
interface PtyProviderOptions {
|
|
5
|
-
ptyHost: IPtyHost;
|
|
6
|
-
sessionId: () => string | undefined;
|
|
7
|
-
onError?: (error: unknown, id: string) => void;
|
|
8
|
-
}
|
|
9
|
-
declare function provideRunnerPty(runtime: ProtocolRuntime, options: PtyProviderOptions): () => void;
|
|
10
|
-
//#endregion
|
|
11
|
-
export { PtyProviderOptions, provideRunnerPty };
|
|
12
|
-
//# sourceMappingURL=ptyProvider.d.cts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ptyProvider.d.cts","names":[],"sources":["../../../../src/adapters/pty/ptyProvider.ts"],"mappings":";;;UAyCiB;EACf,SAAS;EACT;EACA,WAAW,gBAAgB;;iBAGb,iBAAiB,SAAS,iBAAiB,SAAS"}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { IPtyHost } from "../../types/ptyHost.mjs";
|
|
2
|
-
import { ProtocolRuntime } from "@agimon-ai/doompi-extension-contracts/protocol";
|
|
3
|
-
//#region src/adapters/pty/ptyProvider.d.ts
|
|
4
|
-
interface PtyProviderOptions {
|
|
5
|
-
ptyHost: IPtyHost;
|
|
6
|
-
sessionId: () => string | undefined;
|
|
7
|
-
onError?: (error: unknown, id: string) => void;
|
|
8
|
-
}
|
|
9
|
-
declare function provideRunnerPty(runtime: ProtocolRuntime, options: PtyProviderOptions): () => void;
|
|
10
|
-
//#endregion
|
|
11
|
-
export { PtyProviderOptions, provideRunnerPty };
|
|
12
|
-
//# sourceMappingURL=ptyProvider.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ptyProvider.d.mts","names":[],"sources":["../../../../src/adapters/pty/ptyProvider.ts"],"mappings":";;;UAyCiB;EACf,SAAS;EACT;EACA,WAAW,gBAAgB;;iBAGb,iBAAiB,SAAS,iBAAiB,SAAS"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{RunnerPtyData as e,RunnerPtyExit as t,RunnerPtyInput as n,RunnerPtyLaunchProtocol as r,RunnerPtyStop as i}from"@agimon-ai/doompi-extension-contracts/runner-pty";const a=`Runner PTY provider is disposed.`;function o(e,t){return`ext:${e}:${t}`.replaceAll(/[^A-Za-z0-9._-]/gu,`-`)}function s(s,c){let l=new Map,u=!1,d=e=>{let t=l.get(e);t&&(l.delete(e),t.dispose())},f=[s.provide(r,async(n,r)=>{if(u)return{launched:!1,reason:a};let i=c.sessionId();if(!i)return{launched:!1,reason:`No runner session is active.`};let f=o(r.source,n.id);if(l.has(f))return{launched:!1,reason:`A run named ${n.id} is already active.`};try{let r=await c.ptyHost.launch({id:f,name:f,command:n.command,cwd:n.cwd,sessionId:i,...n.cols===void 0?{}:{cols:n.cols},...n.rows===void 0?{}:{rows:n.rows}});if(u){try{await r.stop()}catch(e){process.emitWarning(`Could not stop a stale runner PTY launch: ${String(e)}`)}return{launched:!1,reason:a}}let o=r.onData(t=>{u||s.notify(e,{id:n.id,chunk:t})});return l.set(f,{run:r,dispose:o}),r.completion().then(e=>{u||s.notify(t,{id:n.id,...typeof e.code==`number`?{code:e.code}:{},...e.signal?{signal:String(e.signal)}:{}})}).catch(()=>{u||s.notify(t,{id:n.id,code:-1})}).finally(()=>d(f)),{launched:!0}}catch(e){return u||c.onError?.(e,n.id),{launched:!1,reason:u?a:e instanceof Error?e.message:String(e)}}}),s.onNotification(n,({id:e,text:t},n)=>{l.get(o(n.source,e))?.run.write(`${t}\n`)}),s.onNotification(i,async({id:e},t)=>{let n=o(t.source,e),r=l.get(n);r&&(d(n),await r.run.stop())})];return()=>{if(!u){u=!0;for(let e of f)e();for(let e of l.values())e.dispose();l.clear()}}}export{s as provideRunnerPty};
|
|
2
|
-
//# sourceMappingURL=ptyProvider.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ptyProvider.mjs","names":[],"sources":["../../../../src/adapters/pty/ptyProvider.ts"],"sourcesContent":["/**\n * Lends this package's pseudo terminals to other extensions.\n *\n * doom-runner is the only package that carries `node-pty`, and the only one that\n * knows how to keep a terminal alive for a session and tear it down with one.\n * Rather than have every extension that needs an answerable command grow its own\n * copy of that, they ask over the bus and the terminal stays here.\n *\n * Output is forwarded raw, chunk by chunk, because the caller is usually\n * rendering a tail and needs the partial line a prompt sits on. Reassembling\n * lines is the caller's problem, not the terminal's.\n *\n * AVOID:\n * - Keeping a run past the session that asked for it\n * - Swallowing an exit; a caller waiting on one would hang forever\n */\n\nimport type { ProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport {\n RunnerPtyData,\n RunnerPtyExit,\n RunnerPtyInput,\n RunnerPtyLaunchProtocol,\n RunnerPtyStop,\n} from '@agimon-ai/doompi-extension-contracts/runner-pty';\nimport type { IPtyHost, PtyRun } from '../../types/ptyHost';\n\n/** Namespaced so a borrowed terminal is distinguishable in the runner list. */\nconst RUN_NAME_PREFIX = 'ext';\nconst DISPOSED_REASON = 'Runner PTY provider is disposed.';\n\n/**\n * Collapses a caller and its run id into one safe path segment.\n *\n * The id becomes a log file name, and package-scoped sources carry a `/`, which\n * `path.join` would turn into a directory nobody created.\n */\nfunction runKey(source: string, id: string): string {\n return `${RUN_NAME_PREFIX}:${source}:${id}`.replaceAll(/[^A-Za-z0-9._-]/gu, '-');\n}\n\nexport interface PtyProviderOptions {\n ptyHost: IPtyHost;\n sessionId: () => string | undefined;\n onError?: (error: unknown, id: string) => void;\n}\n\nexport function provideRunnerPty(runtime: ProtocolRuntime, options: PtyProviderOptions): () => void {\n const runs = new Map<string, { run: PtyRun; dispose: () => void }>();\n let disposed = false;\n\n const release = (id: string): void => {\n const entry = runs.get(id);\n if (!entry) return;\n runs.delete(id);\n entry.dispose();\n };\n\n const disposers = [\n runtime.provide(RunnerPtyLaunchProtocol, async (request, identity) => {\n if (disposed) return { launched: false, reason: DISPOSED_REASON };\n const sessionId = options.sessionId();\n if (!sessionId) return { launched: false, reason: 'No runner session is active.' };\n // Keyed by the caller too, so two extensions choosing the same id cannot\n // collide or read each other's output.\n const key = runKey(identity.source, request.id);\n if (runs.has(key)) return { launched: false, reason: `A run named ${request.id} is already active.` };\n try {\n const run = await options.ptyHost.launch({\n id: key,\n // The host keys its runs by name, so this is the unique one rather\n // than the caller's label; two callers naming a run `install` would\n // otherwise clobber each other.\n name: key,\n command: request.command,\n cwd: request.cwd,\n sessionId,\n ...(request.cols === undefined ? {} : { cols: request.cols }),\n ...(request.rows === undefined ? {} : { rows: request.rows }),\n });\n if (disposed) {\n try {\n await run.stop();\n } catch (error) {\n // The provider no longer has a live caller to report to; the owning\n // PTY host performs a second bounded cleanup during root disposal.\n process.emitWarning(`Could not stop a stale runner PTY launch: ${String(error)}`);\n }\n return { launched: false, reason: DISPOSED_REASON };\n }\n const unsubscribe = run.onData((chunk) => {\n if (!disposed) runtime.notify(RunnerPtyData, { id: request.id, chunk });\n });\n runs.set(key, { run, dispose: unsubscribe });\n void run\n .completion()\n .then((result) => {\n if (disposed) return;\n runtime.notify(RunnerPtyExit, {\n id: request.id,\n ...(typeof result.code === 'number' ? { code: result.code } : {}),\n ...(result.signal ? { signal: String(result.signal) } : {}),\n });\n })\n // A caller blocked on the exit would wait forever otherwise, so a\n // failure to observe one is still reported as an exit.\n .catch(() => {\n if (!disposed) runtime.notify(RunnerPtyExit, { id: request.id, code: -1 });\n })\n .finally(() => release(key));\n return { launched: true };\n } catch (error) {\n if (!disposed) options.onError?.(error, request.id);\n return {\n launched: false,\n reason: disposed ? DISPOSED_REASON : error instanceof Error ? error.message : String(error),\n };\n }\n }),\n runtime.onNotification(RunnerPtyInput, ({ id, text }, identity) => {\n runs.get(runKey(identity.source, id))?.run.write(`${text}\\n`);\n }),\n runtime.onNotification(RunnerPtyStop, async ({ id }, identity) => {\n const key = runKey(identity.source, id);\n const entry = runs.get(key);\n if (!entry) return;\n release(key);\n await entry.run.stop();\n }),\n ];\n\n return () => {\n if (disposed) return;\n disposed = true;\n for (const dispose of disposers) dispose();\n for (const entry of runs.values()) entry.dispose();\n runs.clear();\n };\n}\n"],"mappings":"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"}
|