@agimon-ai/doompi-runner 0.0.1-alpha.124 → 0.0.1-alpha.126
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
CHANGED
|
@@ -6,6 +6,8 @@ Part of the [DoomPi distribution](https://www.npmjs.com/package/@agimon-ai/doomp
|
|
|
6
6
|
|
|
7
7
|
Runner replaces Pi's built-in `bash` tool only in sessions that load it. Short commands return
|
|
8
8
|
inline; long commands can be promoted to a background runner instead of blocking the agent.
|
|
9
|
+
In headless sessions, an agent-watched runner keeps background-work ownership until its completion
|
|
10
|
+
wake-up is admitted, so workflow steps cannot finish during a delayed or retried handoff.
|
|
9
11
|
|
|
10
12
|
> **Alpha:** tool and process contracts may change between releases.
|
|
11
13
|
|
|
@@ -154,11 +154,11 @@ function createRunnerServerRuntime(container, directEvents, wakeAgent) {
|
|
|
154
154
|
const publishRuns = async (ownedSessionId) => {
|
|
155
155
|
try {
|
|
156
156
|
const records = await container.runnerRegistry.listAll(ownedSessionId);
|
|
157
|
-
const nextBackgroundWork = records.filter((record) => record.state === "running").map((record) => ({
|
|
157
|
+
const nextBackgroundWork = records.filter((record) => record.state === "running" || watched.has(record.id)).map((record) => ({
|
|
158
158
|
id: record.id,
|
|
159
159
|
sessionId: record.sessionId,
|
|
160
160
|
label: record.name,
|
|
161
|
-
status: record.state
|
|
161
|
+
...record.state === "running" ? { status: record.state } : {}
|
|
162
162
|
}));
|
|
163
163
|
if (JSON.stringify(nextBackgroundWork) !== JSON.stringify(backgroundWorkItems)) {
|
|
164
164
|
backgroundWorkItems = nextBackgroundWork;
|
|
@@ -201,6 +201,7 @@ function createRunnerServerRuntime(container, directEvents, wakeAgent) {
|
|
|
201
201
|
if (record?.state === "running") continue;
|
|
202
202
|
if (record) await wakeAgent?.(require_runnerRegistry.formatRunnerFinished(record), `runner-finished:${JSON.stringify([ownedSessionId, id])}`);
|
|
203
203
|
watched.delete(id);
|
|
204
|
+
requestPublish(ownedSessionId);
|
|
204
205
|
} catch (error) {
|
|
205
206
|
retry = true;
|
|
206
207
|
process.emitWarning(`Could not report finished runner ${id}: ${String(error)}`);
|
|
@@ -227,6 +228,7 @@ function createRunnerServerRuntime(container, directEvents, wakeAgent) {
|
|
|
227
228
|
const ownedSessionId = sessionId;
|
|
228
229
|
if (!wakeAgent || closing || ownedSessionId === void 0) return;
|
|
229
230
|
watched.add(id);
|
|
231
|
+
requestPublish(ownedSessionId);
|
|
230
232
|
unsubscribeWatched ??= container.runnerRegistry.subscribe(() => wakeForFinished(ownedSessionId), ownedSessionId);
|
|
231
233
|
wakeForFinished(ownedSessionId);
|
|
232
234
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.cjs","names":["isRunnerRecord","reconcileActiveRunners","stopRunnerProcess","formatRunnerFinished","DOOM_BACKGROUND_WORK_SERVICE","readDoomBackgroundWorkService","BACKGROUND_WORK_PROVIDER","createRunnerDependencies","createHeadlessBashTool","summarizeLog","createHeadlessRunnersCommand","defineRoot","defineChannel","fs","defineApiRoutes","apiResponse","path","fs","LogReader","RunnerPaths","resolveRunnerStoreDirectory","runnerStateDirFor","Hono","PtyBackendChain","RmuxBackend","TmuxBackend","routes","streamSSE","defineRoute","defineServerCommand","defineServerTool","defineServerPlugin","channelRunnerRuns","rootSession","composeRootHooks","apiRoute","commandRunners","toolBash"],"sources":["../../src/constants/webRunners.ts","../../src/constants/webRunnerRuns.ts","../../src/services/webRunnerRuns/index.ts","../../src/services/runnerServerRuntime/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/root.server.ts","../../src/services/runnersChannel/index.ts","../../src/extensions/(backend)/channel/runnerRuns.server.ts","../../src/constants/runnerLogApi.ts","../../src/constants/webRunnerLog.ts","../../src/services/logTail/index.ts","../../src/types/webRunnerLog.ts","../../src/types/apiRoutes.ts","../../src/services/runnerLogApi/index.ts","../../src/extensions/workspaces/sessions/(backend)/api/runner/route.server.ts","../../src/extensions/workspaces/sessions/(backend)/command/runners.server.ts","../../src/extensions/workspaces/sessions/(backend)/tool/bash.server.ts","../../generated/server.ts"],"sourcesContent":["export const RUNNER_RUNS_TYPE = 'runner_runs';\n","export const FINISHED_RUNNER_RETENTION_MS = 600_000;\n","import { FINISHED_RUNNER_RETENTION_MS } from '../../constants/webRunnerRuns';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport type { RunnerRunView } from '../../types/webRunners';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** How long a finished runner stays in the cockpit's feed after it exited. */\n\n/** One on-disk metadata record, or undefined when the text is not a runner record. */\nexport function parseRunnerRecord(raw: string): RunnerRecord | undefined {\n try {\n const parsed: unknown = JSON.parse(raw);\n return isRunnerRecord(parsed) ? parsed : undefined;\n } catch {\n return undefined; // Mid-write or foreign file; the next scan settles it.\n }\n}\n\nexport function toRunnerRunView(record: RunnerRecord): RunnerRunView {\n return {\n id: record.id,\n name: record.name,\n pid: record.pid,\n command: record.command,\n cwd: record.cwd,\n interactive: record.interactive,\n backend: record.backend,\n state: record.state,\n promoted: record.promoted,\n startedAt: record.startedAt,\n logPath: record.logPath,\n ...(record.exit\n ? {\n exit: {\n reason: record.exit.reason,\n code: record.exit.code,\n signal: record.exit.signal,\n ...(record.exit.terminationReason === undefined\n ? {}\n : { terminationReason: record.exit.terminationReason }),\n ...(record.exit.stopReason === undefined ? {} : { stopReason: record.exit.stopReason }),\n finishedAt: record.exit.finishedAt,\n },\n }\n : {}),\n };\n}\n\nfunction finishedAt(record: RunnerRecord): number {\n const parsed = record.exit === undefined ? Number.NaN : Date.parse(record.exit.finishedAt);\n return Number.isFinite(parsed) ? parsed : Date.parse(record.startedAt);\n}\n\n/**\n * The feed the cockpit shows: running runners first, newest start first, then\n * the recently finished ones, newest exit first. Finished runners older than\n * the retention window are left out; the registry keeps them for the CLI.\n */\nexport function presentRunnerRuns(records: readonly RunnerRecord[], now: number): RunnerRunView[] {\n const running = records\n .filter((record) => record.state === 'running')\n .sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n const finished = records\n .filter((record) => record.state === 'completed' && now - finishedAt(record) <= FINISHED_RUNNER_RETENTION_MS)\n .sort((left, right) => finishedAt(right) - finishedAt(left));\n return [...running, ...finished].map(toRunnerRunView);\n}\n","import {\n DOOM_BACKGROUND_WORK_SERVICE,\n readDoomBackgroundWorkService,\n type BackgroundProviderWorkItem,\n type BackgroundWorkProviderHandle,\n} from '@agimon-ai/doompi-core/backgroundWork';\nimport type { DoomHeadlessActivity, DoomHeadlessExecutionContext } from '@agimon-ai/doompi-core/headless';\nimport type { DoomServerHostService } from '@agimon-ai/doompi-core/serverFacet';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { BACKGROUND_WORK_PROVIDER } from '../../constants/runnerRuntime';\nimport { RUNNER_RUNS_TYPE } from '../../constants/webRunners';\nimport { reconcileActiveRunners, stopRunnerProcess } from '../reconcile';\nimport type { RunnerDependencies } from '../runnerDependencies/type';\nimport { formatRunnerFinished } from '../runnerRecord';\nimport { presentRunnerRuns } from '../webRunnerRuns';\n\nconst COMPLETION_RETRY_MS = 1_000;\n\nexport interface RunnerServerRuntime {\n readonly container: RunnerDependencies;\n readonly activity: DoomHeadlessActivity;\n readonly backgroundWorkPlugin: (context: Context) => void;\n ensureSession(sessionId: string): Promise<void>;\n /** Wakes the agent once when this runner, which it sent to the background, exits. */\n watchRunner(id: string): void;\n dispose(this: void): Promise<void>;\n}\nexport function createRunnerServerRuntime(\n container: RunnerDependencies,\n directEvents: NonNullable<DoomServerHostService['context']['directEvents']>,\n /** Absent when no agent runs in this session, so there is nobody to wake. */\n wakeAgent?: (content: string, requestId: string) => Promise<void>,\n): RunnerServerRuntime {\n let sessionId: string | undefined;\n let supervision: Promise<void> | undefined;\n let runtimeDisposal: Promise<void> | undefined;\n let backgroundWorkItems: BackgroundProviderWorkItem[] = [];\n let backgroundWorkProvider: BackgroundWorkProviderHandle | undefined;\n // Nothing else tells the model a background runner ended, so without this an\n // agent that finished its turn waiting on one idles until a person prompts it.\n const watched = new Set<string>();\n let unsubscribeWatched: (() => void) | undefined;\n let wakes: Promise<void> = Promise.resolve();\n let closing = false;\n let retryTimer: ReturnType<typeof setTimeout> | undefined;\n\n // The activity is optional and source-gated. The retained facet owns the\n // session runtime, so disabling the activity must not tear down its jobs.\n const ensureSession = (requestedSessionId: string): Promise<void> => {\n if (closing) return Promise.reject(new Error('Runner runtime is closing.'));\n if (sessionId !== undefined && sessionId !== requestedSessionId) {\n throw new Error(`Runner runtime belongs to session ${sessionId}.`);\n }\n sessionId ??= requestedSessionId;\n supervision ??= (async () => {\n container.paths.setSessionId(requestedSessionId);\n if ((await container.lifeline.arm(requestedSessionId)) === undefined) {\n throw new Error('Could not arm runner supervision lifeline.');\n }\n const reconciled = await reconcileActiveRunners({\n registry: container.runnerRegistry,\n launcher: container.launcher,\n rmuxBackend: container.rmuxBackend,\n processControl: container.processControl,\n currentHostPid: process.pid,\n startup: false,\n active: await container.runnerRegistry.listBySession(requestedSessionId),\n });\n for (const error of reconciled.errors) process.emitWarning(error);\n if (!closing) {\n unsubscribeRunnerUpdates ??= container.runnerRegistry.subscribe(\n () => requestPublish(requestedSessionId),\n requestedSessionId,\n );\n requestPublish(requestedSessionId);\n }\n })().catch((error: unknown) => {\n supervision = undefined;\n throw error;\n });\n return supervision;\n };\n\n const disposeRuntime = (): Promise<void> => {\n runtimeDisposal ??= (async () => {\n await supervision?.catch((error: unknown) =>\n process.emitWarning(`Could not initialize headless runner supervision: ${String(error)}`),\n );\n const ownedSessionId = sessionId;\n if (ownedSessionId === undefined) return;\n const records = await container.runnerRegistry.listBySession(ownedSessionId).catch(() => []);\n await Promise.all(\n records.map(async (record) => {\n try {\n const stopped = await stopRunnerProcess(record, container.launcher, container.rmuxBackend, {\n registry: container.runnerRegistry,\n intent: {\n reason: 'stopped',\n terminationReason: 'parent_session_cleanup',\n stopReason: 'session ended',\n },\n });\n if (!stopped && container.processControl.isAlive(record.pid)) {\n process.emitWarning(`Could not stop runner ${record.id} during session shutdown`);\n return;\n }\n await container.runnerRegistry.complete(\n record.id,\n { reason: 'stopped', code: null, signal: null, stopReason: 'session ended' },\n ownedSessionId,\n );\n } catch (error) {\n process.emitWarning(`Could not clean up runner ${record.id}: ${String(error)}`);\n }\n }),\n );\n try {\n await container.ptyHost.disposeAll();\n } finally {\n container.lifeline.dispose();\n container.runnerRegistry.close();\n }\n })();\n return runtimeDisposal;\n };\n\n const publishRuns = async (ownedSessionId: string): Promise<void> => {\n try {\n const records = await container.runnerRegistry.listAll(ownedSessionId);\n const nextBackgroundWork = records\n .filter((record) => record.state === 'running')\n .map((record) => ({\n id: record.id,\n sessionId: record.sessionId,\n label: record.name,\n status: record.state,\n }));\n if (JSON.stringify(nextBackgroundWork) !== JSON.stringify(backgroundWorkItems)) {\n backgroundWorkItems = nextBackgroundWork;\n backgroundWorkProvider?.update();\n }\n directEvents.publish(RUNNER_RUNS_TYPE, ownedSessionId, {\n runs: presentRunnerRuns(records, Date.now()),\n });\n } catch (error) {\n process.emitWarning(`Could not publish runner state: ${String(error)}`);\n }\n };\n let publishInFlight: Promise<void> | undefined;\n let publishAgain = false;\n let unsubscribeRunnerUpdates: (() => void) | undefined;\n const requestPublish = (ownedSessionId: string): void => {\n if (closing) return;\n publishAgain = true;\n if (publishInFlight !== undefined) return;\n publishInFlight = (async () => {\n do {\n publishAgain = false;\n await publishRuns(ownedSessionId);\n } while (publishAgain && !closing);\n })()\n .catch((error: unknown) => {\n process.emitWarning(`Could not schedule runner state publication: ${String(error)}`);\n })\n .finally(() => {\n publishInFlight = undefined;\n if (publishAgain) requestPublish(ownedSessionId);\n });\n };\n\n // Sequenced so two runners exiting in the same tick each wake the agent once.\n const wakeForFinished = (ownedSessionId: string): void => {\n if (closing) return;\n wakes = wakes\n .then(async () => {\n if (closing) return;\n let retry = false;\n // Deleting the entry being visited is safe for a Set iterator.\n for (const id of watched) {\n if (closing) return;\n try {\n const record = await container.runnerRegistry.get(id, ownedSessionId);\n if (closing) return;\n if (record?.state === 'running') continue;\n // A record swept before it was seen leaves nothing to report.\n if (record)\n await wakeAgent?.(\n formatRunnerFinished(record),\n `runner-finished:${JSON.stringify([ownedSessionId, id])}`,\n );\n watched.delete(id);\n } catch (error) {\n retry = true;\n process.emitWarning(`Could not report finished runner ${id}: ${String(error)}`);\n }\n }\n if (retry && !closing && retryTimer === undefined) {\n retryTimer = setTimeout(() => {\n retryTimer = undefined;\n wakeForFinished(ownedSessionId);\n }, COMPLETION_RETRY_MS);\n retryTimer.unref?.();\n }\n if (watched.size === 0) {\n clearTimeout(retryTimer);\n retryTimer = undefined;\n unsubscribeWatched?.();\n unsubscribeWatched = undefined;\n }\n })\n .catch((error: unknown) => {\n process.emitWarning(`Could not report a finished runner: ${String(error)}`);\n });\n };\n\n const watchRunner = (id: string): void => {\n const ownedSessionId = sessionId;\n if (!wakeAgent || closing || ownedSessionId === undefined) return;\n watched.add(id);\n // Independent of the optional activity: the wake-up must not depend on a\n // browser watching the runner list.\n unsubscribeWatched ??= container.runnerRegistry.subscribe(() => wakeForFinished(ownedSessionId), ownedSessionId);\n // It may already have exited between promotion and subscription.\n wakeForFinished(ownedSessionId);\n };\n\n const activity: DoomHeadlessActivity = {\n name: 'runner',\n start: async (executionContext: DoomHeadlessExecutionContext) => {\n await ensureSession(executionContext.sessionId);\n requestPublish(executionContext.sessionId);\n return () => {\n // Let the next optional activation reconcile again, while keeping\n // the retained runtime and all child job supervisors alive.\n supervision = undefined;\n };\n },\n };\n return {\n container,\n activity,\n ensureSession,\n watchRunner,\n backgroundWorkPlugin: (context) => {\n context.inject([DOOM_BACKGROUND_WORK_SERVICE], (serviceContext) => {\n const service = readDoomBackgroundWorkService(serviceContext);\n if (!service) return undefined;\n const registration = service.register({\n provider: BACKGROUND_WORK_PROVIDER,\n listActiveWork: () => backgroundWorkItems,\n });\n backgroundWorkProvider = registration;\n return () => {\n registration.dispose();\n if (backgroundWorkProvider === registration) backgroundWorkProvider = undefined;\n };\n });\n },\n async dispose() {\n // Session cleanup completes every runner it stops, and none of those may\n // start a turn in a session that is going away.\n closing = true;\n clearTimeout(retryTimer);\n retryTimer = undefined;\n watched.clear();\n unsubscribeWatched?.();\n unsubscribeWatched = undefined;\n unsubscribeRunnerUpdates?.();\n unsubscribeRunnerUpdates = undefined;\n await wakes;\n await publishInFlight;\n await disposeRuntime();\n },\n };\n}\n","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { createHeadlessBashTool, createHeadlessRunnersCommand } from '../../../../../services/headless';\nimport { summarizeLog } from '../../../../../services/logReader';\nimport { createRunnerDependencies } from '../../../../../services/runnerDependencies';\nimport { createRunnerServerRuntime } from '../../../../../services/runnerServerRuntime';\n\nexport const RUNNER_SERVER_SCOPE_SERVICE = 'doom/runner-server-scope';\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n 'doom/runner-server-scope': RunnerServerScope;\n }\n}\n\nexport const createRunnerServerRoot = ({ host, agent, signal }: DoomServerPluginContext) => {\n if (!host.context.directEvents) throw new Error('Runner headless facet requires the session direct event bus.');\n if (!host.context.cwd || !host.context.environment) {\n throw new Error('Runner headless facet requires session cwd and environment.');\n }\n const dependencies = createRunnerDependencies({\n cwd: host.context.cwd,\n environment: host.context.environment,\n ...(host.context.executionBudget === undefined ? {} : { executionBudget: host.context.executionBudget }),\n });\n const execution = agent?.context;\n const wakeAgent =\n execution &&\n (async (content: string, requestId: string): Promise<void> => {\n try {\n // 'steer' joins a running turn and starts one on an idle session.\n // Reuse completion identity across retries and acknowledge admission, not settlement.\n if (!execution.session.admitPrompt) throw new Error('The session cannot admit a runner notification.');\n await execution.session.admitPrompt(content, 'steer', undefined, requestId);\n } catch (error) {\n // Keep completion ownership with the watcher until admission succeeds.\n await Promise.resolve()\n .then(() => execution.client.notify({ body: String(error), level: 'warning' }))\n .catch(() => undefined);\n throw error;\n }\n });\n const runtime = createRunnerServerRuntime(dependencies, host.context.directEvents, wakeAgent);\n const bash = (wakeOnExit: boolean) =>\n createHeadlessBashTool(\n {\n run: async (request) => {\n await runtime.ensureSession(request.sessionId);\n const result = await dependencies.bashRunService.run({\n ...request,\n ...(signal ? { signal: request.signal ? AbortSignal.any([signal, request.signal]) : signal } : {}),\n });\n // Without this, nothing tells the agent the runner ended.\n if (wakeOnExit && result.kind === 'promoted') runtime.watchRunner(result.id);\n return result;\n },\n },\n summarizeLog,\n );\n const value = {\n tool: bash(true),\n // Remote MCP runners must not wake the local agent; the remote caller polls instead.\n remoteTool: bash(false),\n command: createHeadlessRunnersCommand(dependencies),\n };\n return {\n value,\n services: [\n runtime.backgroundWorkPlugin,\n (context: Context) => {\n context.plugin((providerContext) => providerContext.provide(RUNNER_SERVER_SCOPE_SERVICE, value));\n },\n ],\n activities: [runtime.activity],\n onStop: runtime.dispose,\n };\n};\n\nexport type RunnerServerScope = Awaited<ReturnType<typeof createRunnerServerRoot>>['value'];\n","import { defineRoot } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { createRunnerServerRoot } from './_lib/serverRoot';\n\nexport default defineRoot(createRunnerServerRoot);\n","import type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { RUNNER_RUNS_TYPE } from '../../constants/webRunners';\nimport { type RunnerRunView } from '../../types/webRunners';\n\nfunction runnerPayload(value: unknown): value is { runs: RunnerRunView[] } {\n return typeof value === 'object' && value !== null && Array.isArray((value as { runs?: unknown }).runs);\n}\n\n/**\n * The runners data channel consumes lifecycle-owned snapshots from the session\n * server. Durable registry files are read by the owning session only, never by\n * this hub channel.\n */\nexport function createRunnersChannel(): DoomHubChannel {\n return {\n frameType: RUNNER_RUNS_TYPE,\n start(host) {\n const latest = new Map<string, RunnerRunView[]>();\n const subscriptions = new Map<string, () => void>();\n const source: DoomHubChannelSource = {\n payloadFor(scope) {\n const runs = latest.get(scope.sessionId);\n return runs === undefined ? undefined : { runs };\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n latest.delete(scope.sessionId);\n const unsubscribe = host.directEvents.subscribe(\n RUNNER_RUNS_TYPE,\n scope.sessionId,\n (payload) => {\n if (!runnerPayload(payload)) return;\n latest.set(scope.sessionId, payload.runs);\n host.publish(scope.sessionId, payload);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n };\n return source;\n },\n };\n}\n","import { defineChannel } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomHubChannel } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { createRunnersChannel } from '../../../services/runnersChannel';\nexport default defineChannel(function runnerRunsChannel(): DoomHubChannel {\n return createRunnersChannel();\n});\n","export const LOG_DIR_NAME = 'logs';\nexport const STATE_EXTENSION = '.json';\nexport const RUNNING_STATE = 'running';\nexport const STATE_POLL_MS = 1000;\nexport const HEARTBEAT_MS = 15_000;\nexport const DEFAULT_LINES = 200;\nexport const MAX_LINES = 5000;\nexport const SAFE_SEGMENT = /^[\\w.@-]+$/;\nexport const SCREEN_POLL_MS = 100;\nexport const RAW_LOG_SUFFIX = '.raw';\nexport const MAX_INPUT_CHARS = 4096;\nexport const NOT_ATTACHABLE = 'No attachable runner by that name in this session.';\n","export const RUNNER_API_BASE_PATH = 'runner';\nexport const RUN_ID_PARAM = 'runId';\nexport const RUNNER_SCREEN_EVENT = 'screen';\nexport const RUNNER_LOG_STREAM_EVENT = 'append';\nexport const RUNNER_LOG_PING_EVENT = 'ping';\n","import fs from 'node:fs';\n\nimport { POLL_MS } from '../../constants/logTail';\nimport type { ILogTail, LogTailHandle, LogTailOptions } from '../../types/logTail';\n\n/** How often the tail restats the file; the same cadence the CLI's --follow uses. */\n\n/**\n * Polling tail: read the delta between the last offset and the file's current\n * size, and hand back only complete lines.\n *\n * Same reliability posture as this package's runner watcher: the poll is the\n * mechanism, not an accelerator for a change notification. fs.watchFile only\n * fires when it observes a difference from a baseline it took itself, so a\n * write that lands before its first stat is a change it never reports; a\n * restat every tick has no such window.\n *\n * A write that lands mid-line is common, so the trailing fragment is held\n * until its newline arrives rather than emitted as a line that will change.\n */\nexport class LogTail implements ILogTail {\n follow(logPath: string, options: LogTailOptions): LogTailHandle {\n let offset = options.from;\n let pending = '';\n let closed = false;\n\n const drain = (size: number): void => {\n // A shrunken file was rotated or truncated; start over rather than\n // reading from an offset that now points into different content.\n if (size < offset) {\n offset = 0;\n pending = '';\n }\n if (size === offset) return;\n const length = size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = fs.openSync(logPath, 'r');\n let bytesRead: number;\n try {\n bytesRead = fs.readSync(descriptor, buffer, 0, length, offset);\n } finally {\n fs.closeSync(descriptor);\n }\n offset = size;\n if (bytesRead <= 0) return;\n pending += buffer.subarray(0, bytesRead).toString('utf8');\n const lines = pending.split('\\n');\n pending = lines.pop() ?? '';\n if (lines.length > 0) options.onLines(lines, offset - Buffer.byteLength(pending, 'utf8'));\n };\n\n const tick = (): void => {\n if (closed) return;\n try {\n drain(fs.statSync(logPath).size);\n } catch (error) {\n // The runner has not opened its log yet; the next tick settles it.\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;\n options.onError(error instanceof Error ? error : new Error(String(error)));\n }\n };\n\n const timer = setInterval(tick, POLL_MS);\n // Following a log must never be the reason a process stays alive.\n timer.unref?.();\n return {\n close() {\n if (closed) return;\n closed = true;\n clearInterval(timer);\n },\n };\n }\n}\n","import type { LogSlice } from './logReader';\n\n/**\n * The runner log API, shared by this package's session-scoped routes and its\n * cockpit plugin. The two halves run in different processes, so the wire\n * vocabulary is declared here: the browser half may reach `src/types` and\n * nothing else on the server side.\n *\n * Where the routes live is not here. That is `src/types/apiRoutes`, which both\n * halves read, and the generated client turns into the URLs the page addresses.\n * The file a run writes to is still read from its metadata record by the server\n * alone.\n */\n\n/**\n * One chunk of an attached pane's output.\n *\n * Base64 because these are terminal bytes, escape sequences and all, and a\n * server-sent event is a line-oriented text frame that would mangle them.\n */\nexport interface RunnerScreenEvent {\n chunk: string;\n /** Byte offset just past this chunk, so a reconnect resumes rather than replays. */\n offset?: number;\n /** Set once the runner exits, so the page can stop watching. */\n ended?: boolean;\n}\n\n/** The body of a keystroke POST. Text, not key codes: the pane wants bytes. */\nexport interface RunnerInputRequest {\n text: string;\n}\n\n/** The named SSE event carrying a RunnerScreenEvent payload. */\n\n/**\n * A log request as query parameters. `grep` is a literal substring, not a\n * regular expression, because that is what the reader matches; `lines` bounds\n * the answer to the last N lines of whatever survived the filter.\n */\nexport interface RunnerLogQueryParams {\n lines?: number;\n grep?: string;\n ignoreCase?: boolean;\n contextLines?: number;\n}\n\n/** The slice the log route answers with, plus which runner it came from. */\nexport interface RunnerLogResponse extends LogSlice {\n runId: string;\n /** True while the runner is still writing, so the page knows following is worth offering. */\n running: boolean;\n}\n\n/**\n * One server-sent event on the follow stream: the lines appended since the last\n * one. The stream never filters: a page that sets a query stops following and\n * reads a filtered slice instead, because grep with context is a whole-file\n * answer and cannot be assembled from a chunk at a time.\n */\nexport interface RunnerLogStreamEvent {\n /** Appended lines, in order; empty when the event only reports the runner ending. */\n lines: string[];\n /**\n * Byte offset just past the last line in this event.\n *\n * A follower that loses the stream resumes here. Without it the page could\n * only fall back to the offset its last slice reported, which every line\n * delivered since has already moved past, so a reconnect would replay them.\n */\n offset?: number;\n /** Set once the runner exits, so the page can stop following without polling the run list. */\n ended?: boolean;\n}\n\n/** The named SSE event carrying a RunnerLogStreamEvent payload. */\n\n/**\n * The named SSE event that carries nothing.\n *\n * A runner can be alive and silent for minutes, which on the wire is\n * indistinguishable from a connection that died. This is sent on a timer so\n * the socket keeps proving itself. Readers that only listen for `append`\n * ignore it, which is what makes it safe to add.\n */\n\n/** Query parameter names, shared so the page and the route cannot drift apart. */\nexport const RUNNER_LOG_PARAMS = {\n lines: 'lines',\n grep: 'grep',\n ignoreCase: 'ignoreCase',\n contextLines: 'contextLines',\n from: 'from',\n} as const;\n","import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport { RUN_ID_PARAM } from '../constants/webRunnerLog';\nimport { RUNNER_LOG_PARAMS, type RunnerLogResponse } from './webRunnerLog';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path: the build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/runner/`, so the mount is\n * stated once, by the folder that creates it. This package stated it four times\n * before, once per URL builder, each of them re-spelling `/plugins/` and the\n * session prefix by hand.\n *\n * The Hono app registers these paths verbatim and the browser addresses them,\n * so a path cannot move on one side alone. `:runId` is Hono's own parameter\n * syntax for that reason; a caller supplies the value as `params`, and the\n * client encodes the segment.\n */\nexport default defineApiRoutes({\n log: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/log`,\n query: [\n RUNNER_LOG_PARAMS.lines,\n RUNNER_LOG_PARAMS.grep,\n RUNNER_LOG_PARAMS.ignoreCase,\n RUNNER_LOG_PARAMS.contextLines,\n ],\n response: apiResponse<RunnerLogResponse>(),\n },\n /**\n * The live tail of that same log.\n *\n * `stream: true`, so the client exposes a URL and no call method: the\n * consumer is the browser's own `EventSource`, and a relayed call would\n * buffer a stream that never ends.\n */\n logStream: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/log/stream`,\n query: [RUNNER_LOG_PARAMS.from],\n stream: true,\n },\n /** The unscrubbed pane bytes, for a terminal emulator rather than a reader. */\n screenStream: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/screen/stream`,\n query: [RUNNER_LOG_PARAMS.from],\n stream: true,\n },\n input: {\n method: 'POST',\n path: `/runners/:${RUN_ID_PARAM}/screen/input`,\n },\n});\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/packageApi';\nimport { Hono } from 'hono';\nimport { streamSSE } from 'hono/streaming';\n\nimport {\n DEFAULT_LINES,\n HEARTBEAT_MS,\n LOG_DIR_NAME,\n MAX_INPUT_CHARS,\n MAX_LINES,\n NOT_ATTACHABLE,\n RAW_LOG_SUFFIX,\n RUNNING_STATE,\n SAFE_SEGMENT,\n SCREEN_POLL_MS,\n STATE_EXTENSION,\n STATE_POLL_MS,\n} from '../../constants/runnerLogApi';\nimport {\n RUN_ID_PARAM,\n RUNNER_API_BASE_PATH,\n RUNNER_LOG_PING_EVENT,\n RUNNER_LOG_STREAM_EVENT,\n RUNNER_SCREEN_EVENT,\n} from '../../constants/webRunnerLog';\nimport { LogReader } from '../../services/logReader';\nimport { LogTail } from '../../services/logTail';\nimport { PtyBackendChain } from '../../services/ptyBackendChain';\nimport { RmuxBackend } from '../../services/rmuxBackend';\nimport { RunnerPaths, resolveRunnerStoreDirectory, runnerStateDirFor } from '../../services/runnerPaths';\nimport { TmuxBackend } from '../../services/tmuxBackend';\nimport { parseRunnerRecord } from '../../services/webRunnerRuns';\nimport routes from '../../types/apiRoutes';\nimport type { ILogReader, LogQuery } from '../../types/logReader';\nimport type { ILogTail } from '../../types/logTail';\nimport type { IRmuxBackend } from '../../types/rmuxBackend';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport {\n RUNNER_LOG_PARAMS,\n type RunnerLogResponse,\n type RunnerLogStreamEvent,\n type RunnerScreenEvent,\n} from '../../types/webRunnerLog';\n\n/** How often a follow re-reads the record to notice the runner exiting. */\n/**\n * How often a silent but living stream proves it is still there. Short enough\n * that an idle proxy does not reap the socket, long enough to cost nothing.\n */\n/** Kept in step with LogQuery's own default, so an unasked-for tail is the same size everywhere. */\n/** A page that asks for more than this gets this; the whole point of the route is a bounded read. */\n/** Path segments the client supplies are names, never paths. */\n/** Backends that expose a pane another process can read and write. */\nconst MULTIPLEXER_BACKENDS: ReadonlySet<string> = new Set(['rmux', 'tmux']);\n/** How often an attached pane's new bytes are picked up. */\n/** The unscrubbed copy the runner's sink writes beside the log, for interactive runs. */\n/** One keystroke batch; a paste is not a file transfer. */\n/** Said the same way for every reason, so the route never reports which check failed. */\n\nexport interface RunnerLogApiOptions {\n /** The session these routes answer for; the host owns exactly one. */\n sessionId: string;\n /** The store root; defaults to this environment's agent directory. */\n storeDir?: string;\n environment?: Readonly<Record<string, string | undefined>>;\n cwd?: string;\n logReader?: ILogReader;\n logTail?: ILogTail;\n /**\n * Reaches a live pane to read its screen and send it input.\n *\n * A session API is handed nothing but its session id, so it cannot touch the\n * in-process PTY handles the extension holds. This talks to the multiplexer\n * over its own socket instead, the same way the CLI's `input` verb does,\n * which is what makes an attached pane reachable across processes at all.\n */\n pane?: IRmuxBackend;\n}\n\n/** Rejects anything that could climb out of the session's own directory before a path is built. */\nfunction isSafeSegment(value: string): boolean {\n return value !== '' && value !== '.' && value !== '..' && SAFE_SEGMENT.test(value);\n}\n\n/**\n * The log file a run wrote, or undefined when there is no such run or its\n * record points somewhere it has no business pointing.\n *\n * The record names its own log path, and that record is written by another\n * process, so the path is checked rather than trusted: it must sit inside this\n * session's own logs directory. Without that check a doctored record would\n * turn this route into an arbitrary file read.\n */\nfunction resolveLogFile(stateDir: string, logsDir: string, sessionId: string, runId: string): RunnerRecord | undefined {\n const statePath = path.join(stateDir, `${runId}${STATE_EXTENSION}`);\n let raw: string;\n try {\n raw = fs.readFileSync(statePath, 'utf8');\n } catch {\n return undefined; // No such run in this session; the route answers 404.\n }\n const record = parseRunnerRecord(raw);\n if (record === undefined || record.id !== runId || record.sessionId !== sessionId) return undefined;\n const resolved = path.resolve(record.logPath);\n if (resolved !== logsDir && !resolved.startsWith(`${logsDir}${path.sep}`)) return undefined;\n return record;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction numberParam(value: string | undefined, fallback: number, max: number): number {\n const parsed = value === undefined ? Number.NaN : Number.parseInt(value, 10);\n if (!Number.isFinite(parsed) || parsed < 0) return fallback;\n return Math.min(parsed, max);\n}\n\nfunction logQueryOf(url: URL): LogQuery {\n const grep = url.searchParams.get(RUNNER_LOG_PARAMS.grep) ?? undefined;\n return {\n lines: numberParam(url.searchParams.get(RUNNER_LOG_PARAMS.lines) ?? undefined, DEFAULT_LINES, MAX_LINES),\n ...(grep === undefined || grep === '' ? {} : { grep }),\n ignoreCase: url.searchParams.get(RUNNER_LOG_PARAMS.ignoreCase) === 'true',\n contextLines: numberParam(url.searchParams.get(RUNNER_LOG_PARAMS.contextLines) ?? undefined, 0, MAX_LINES),\n };\n}\n\n/**\n * This package's HTTP surface: one runner's log, as a bounded slice and as a\n * live tail.\n *\n * The routes are mounted inside one session's own server, so they name a runner\n * and nothing else; the session is the host, not a parameter. The reader only\n * ever returns the last N lines of what matched, so both routes stay bounded no\n * matter how large the file on disk is.\n *\n * Paths come from src/types/apiRoutes, the one table the browser's generated\n * client reads too, so this app and the cockpit cannot disagree about where a\n * route lives. They stay relative to the mount the host strips.\n */\nexport function createRunnerLogApi(options: RunnerLogApiOptions): Hono {\n const logReader = options.logReader ?? new LogReader();\n const logTail = options.logTail ?? new LogTail();\n const sessionId = options.sessionId;\n const paths = new RunnerPaths(options.cwd, options.environment);\n const storeDir = options.storeDir ?? resolveRunnerStoreDirectory(options.environment ?? process.env, options.cwd);\n const stateDir =\n options.storeDir === undefined ? paths.stateDirectory(sessionId) : runnerStateDirFor(storeDir, sessionId);\n const logsDir =\n options.storeDir === undefined ? paths.logDirectory(sessionId) : path.join(storeDir, sessionId, LOG_DIR_NAME);\n const app = new Hono();\n\n // Built on first use, not on mount: most sessions never attach to a pane,\n // and constructing this probes the filesystem for a multiplexer binary.\n let paneBackend: IRmuxBackend | undefined = options.pane;\n const pane = (): IRmuxBackend => {\n paneBackend ??= (() => {\n const paths = new RunnerPaths(options.cwd, options.environment);\n return new PtyBackendChain(new RmuxBackend(paths), new TmuxBackend(paths));\n })();\n return paneBackend;\n };\n\n /** The record behind a request, or undefined when this session has no such run. */\n const runnerOf = (runId: string): RunnerRecord | undefined =>\n isSafeSegment(runId) ? resolveLogFile(stateDir, logsDir, sessionId, runId) : undefined;\n\n /**\n * The pane a request may attach to.\n *\n * Same posture as the log routes: the record is written by another process,\n * so nothing on it is trusted until it is checked. The target must be one\n * this package owns, the run must still be up, and it must have been started\n * interactive. A non-interactive run has no terminal to type into, and a\n * subprocess-backed run has no pane at all.\n */\n const attachableOf = (runId: string): { record: RunnerRecord; target: string } | undefined => {\n const record = runnerOf(runId);\n if (record === undefined || record.state !== RUNNING_STATE || record.interactive !== true) return undefined;\n const target = record.backendTarget;\n if (target === undefined || !MULTIPLEXER_BACKENDS.has(record.backend) || !isSafeSegment(target)) return undefined;\n return { record, target };\n };\n app.get(routes.log.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const record = runnerOf(runId);\n if (record === undefined) return context.json({ error: `No runner '${runId}' in this session.` }, 404);\n const slice = logReader.read(record.logPath, logQueryOf(new URL(context.req.url)));\n const body: RunnerLogResponse = { ...slice, runId, running: record.state === RUNNING_STATE };\n return context.json(body);\n });\n\n app.get(routes.logStream.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const record = runnerOf(runId);\n if (record === undefined) return context.json({ error: `No runner '${runId}' in this session.` }, 404);\n const from = numberParam(\n new URL(context.req.url).searchParams.get(RUNNER_LOG_PARAMS.from) ?? undefined,\n 0,\n Number.MAX_SAFE_INTEGER,\n );\n\n return streamSSE(context, async (stream) => {\n const send = (event: RunnerLogStreamEvent): Promise<void> =>\n stream.writeSSE({ event: RUNNER_LOG_STREAM_EVENT, data: JSON.stringify(event) });\n\n // The tail and the state poll both push onto one queue, so the stream\n // writes in the order things happened rather than interleaving two\n // writers onto the same socket.\n let pump: Promise<void> = Promise.resolve();\n const queue = (event: RunnerLogStreamEvent): void => {\n pump = pump.then(() => send(event)).catch(() => undefined);\n };\n\n let finish = (): void => undefined;\n const done = new Promise<void>((resolve) => {\n finish = resolve;\n });\n const handle = logTail.follow(record.logPath, {\n from,\n onLines: (lines, completeThrough) => queue({ lines, offset: completeThrough }),\n onError: () => finish(),\n });\n // A finished runner writes nothing more, so the page is told once and\n // the stream ends rather than holding a socket open forever.\n const poll = setInterval(() => {\n const current = runnerOf(runId);\n if (current !== undefined && current.state === RUNNING_STATE) return;\n queue({ lines: [], ended: true });\n finish();\n }, STATE_POLL_MS);\n // A runner can be alive and produce nothing for minutes. Without this the\n // page cannot tell that from a stream that quietly died.\n const heartbeat = setInterval(() => {\n pump = pump.then(() => stream.writeSSE({ event: RUNNER_LOG_PING_EVENT, data: '' })).catch(() => undefined);\n }, HEARTBEAT_MS);\n\n stream.onAbort(() => finish());\n await done;\n clearInterval(poll);\n clearInterval(heartbeat);\n handle.close();\n await pump;\n });\n });\n\n app.get(routes.screenStream.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const attachable = attachableOf(runId);\n if (attachable === undefined) return context.json({ error: NOT_ATTACHABLE }, 404);\n // The record's log path is already proven to sit inside this session's own\n // logs directory, and the raw copy is that same path with a suffix, so it\n // cannot point anywhere new.\n const rawPath = `${attachable.record.logPath}${RAW_LOG_SUFFIX}`;\n const from = numberParam(\n new URL(context.req.url).searchParams.get(RUNNER_LOG_PARAMS.from) ?? undefined,\n 0,\n Number.MAX_SAFE_INTEGER,\n );\n\n return streamSSE(context, async (stream) => {\n let pump: Promise<void> = Promise.resolve();\n const queue = (event: RunnerScreenEvent): void => {\n pump = pump\n .then(() => stream.writeSSE({ event: RUNNER_SCREEN_EVENT, data: JSON.stringify(event) }))\n .catch(() => undefined);\n };\n\n let finish = (): void => undefined;\n const done = new Promise<void>((resolve) => {\n finish = resolve;\n });\n\n let offset = from;\n const drain = (): void => {\n let size: number;\n try {\n size = fs.statSync(rawPath).size;\n } catch {\n return; // The sink has not opened the file yet; the next tick settles it.\n }\n // The sink starts the file over when it passes its ceiling, so a\n // smaller file is a fresh one rather than a bad offset.\n if (size < offset) offset = 0;\n if (size === offset) return;\n const length = size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = fs.openSync(rawPath, 'r');\n let read: number;\n try {\n read = fs.readSync(descriptor, buffer, 0, length, offset);\n } finally {\n fs.closeSync(descriptor);\n }\n if (read <= 0) return;\n offset += read;\n // Bytes, not lines: a terminal's escape sequences straddle newlines and\n // mean nothing once split on them.\n queue({ chunk: buffer.subarray(0, read).toString('base64'), offset });\n };\n\n const poll = setInterval(() => {\n const current = runnerOf(runId);\n drain();\n if (current === undefined || current.state !== RUNNING_STATE) {\n queue({ chunk: '', offset, ended: true });\n finish();\n }\n }, SCREEN_POLL_MS);\n\n stream.onAbort(() => finish());\n await done;\n clearInterval(poll);\n await pump;\n });\n });\n\n /**\n * Types into a live pane.\n *\n * This is the one route in the package that writes rather than reads, so it\n * is deliberately narrow: an owned target, still running, started\n * interactive, and a body that is nothing but text.\n */\n app.post(routes.input.path, async (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const attachable = attachableOf(runId);\n if (attachable === undefined) return context.json({ error: NOT_ATTACHABLE }, 404);\n\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'Expected a JSON body carrying text.' }, 400);\n }\n const text = isRecord(body) ? body.text : undefined;\n if (typeof text !== 'string' || text === '') return context.json({ error: 'Expected a non-empty text.' }, 400);\n if (text.length > MAX_INPUT_CHARS) return context.json({ error: 'That is more input than a pane takes.' }, 413);\n\n const delivered = await pane().input(attachable.target, text);\n if (!delivered) return context.json({ error: 'The runner did not take the input.' }, 502);\n return context.json({ delivered: true });\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built session entry. */\nexport const api: DoomApi = {\n basePath: RUNNER_API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n // The host is one session's server, so its id is the whole scope; a hub\n // would have handed no session at all, and there is nothing to answer.\n const app = createRunnerLogApi({\n sessionId: context.sessionId ?? '',\n cwd: context.cwd,\n environment: context.environment,\n });\n return {\n fetch: (request) => app.fetch(request),\n // Nothing outlives a request: a follow's watch and its poll are both\n // torn down when its own stream ends or the socket aborts.\n close: () => undefined,\n };\n },\n};\n","import { defineRoute } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { api } from '../../../../../../services/runnerLogApi';\n\n/**\n * Mounts this package's HTTP API at `runner`, which the folder names.\n *\n * The folder is the declaration: the build reads the base path off it and hands\n * the browser a client bound to the same segment, so the mount is stated once\n * rather than in a constant and in four URL builders that were each free to\n * drift.\n */\nexport default defineRoute(api);\n","import { defineServerCommand, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { RunnerServerScope } from '../_lib/serverRoot';\nexport default defineServerCommand(\n (context: WithRoot<DoomServerPluginContext, RunnerServerScope>) => context.root.command,\n);\n","import { defineServerTool, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { RunnerServerScope } from '../_lib/serverRoot';\nexport default defineServerTool((context: WithRoot<DoomServerPluginContext, RunnerServerScope>) => context.root.tool);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\nimport { composeRootHooks } from '@agimon-ai/doompi-core/extensionFile';\n\nimport rootSession from '../src/extensions/workspaces/sessions/(backend)/root.server';\nimport channelRunnerRuns from '../src/extensions/(backend)/channel/runnerRuns.server';\nimport apiRoute from '../src/extensions/workspaces/sessions/(backend)/api/runner/route.server';\nimport commandRunners from '../src/extensions/workspaces/sessions/(backend)/command/runners.server';\nimport toolBash from '../src/extensions/workspaces/sessions/(backend)/tool/bash.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/doompi-runner',\n global: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n }),\n workspace: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n }),\n session: async (context) => {\n const scopeSession = await rootSession(context);\n const contextSession = { ...context, root: scopeSession.value };\n return {\n ...composeRootHooks(scopeSession),\n get api(): DoomServerSessionPlugin['api'] { return [at(apiRoute, contextSession)]; },\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n get commands(): DoomServerSessionPlugin['commands'] { return [via({ name: 'runners' }, at(commandRunners, contextSession))]; },\n get tools(): DoomServerSessionPlugin['tools'] { return [via({ name: 'bash' }, at(toolBash, contextSession))]; },\n services: [...(scopeSession.services ?? [])],\n get activities(): DoomServerSessionPlugin['activities'] { return [...(scopeSession.activities ?? [])]; },\n };\n },\n});\n\nexport default facet;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.cjs","names":["isRunnerRecord","reconcileActiveRunners","stopRunnerProcess","formatRunnerFinished","DOOM_BACKGROUND_WORK_SERVICE","readDoomBackgroundWorkService","BACKGROUND_WORK_PROVIDER","createRunnerDependencies","createHeadlessBashTool","summarizeLog","createHeadlessRunnersCommand","defineRoot","defineChannel","fs","defineApiRoutes","apiResponse","path","fs","LogReader","RunnerPaths","resolveRunnerStoreDirectory","runnerStateDirFor","Hono","PtyBackendChain","RmuxBackend","TmuxBackend","routes","streamSSE","defineRoute","defineServerCommand","defineServerTool","defineServerPlugin","channelRunnerRuns","rootSession","composeRootHooks","apiRoute","commandRunners","toolBash"],"sources":["../../src/constants/webRunners.ts","../../src/constants/webRunnerRuns.ts","../../src/services/webRunnerRuns/index.ts","../../src/services/runnerServerRuntime/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/root.server.ts","../../src/services/runnersChannel/index.ts","../../src/extensions/(backend)/channel/runnerRuns.server.ts","../../src/constants/runnerLogApi.ts","../../src/constants/webRunnerLog.ts","../../src/services/logTail/index.ts","../../src/types/webRunnerLog.ts","../../src/types/apiRoutes.ts","../../src/services/runnerLogApi/index.ts","../../src/extensions/workspaces/sessions/(backend)/api/runner/route.server.ts","../../src/extensions/workspaces/sessions/(backend)/command/runners.server.ts","../../src/extensions/workspaces/sessions/(backend)/tool/bash.server.ts","../../generated/server.ts"],"sourcesContent":["export const RUNNER_RUNS_TYPE = 'runner_runs';\n","export const FINISHED_RUNNER_RETENTION_MS = 600_000;\n","import { FINISHED_RUNNER_RETENTION_MS } from '../../constants/webRunnerRuns';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport type { RunnerRunView } from '../../types/webRunners';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** How long a finished runner stays in the cockpit's feed after it exited. */\n\n/** One on-disk metadata record, or undefined when the text is not a runner record. */\nexport function parseRunnerRecord(raw: string): RunnerRecord | undefined {\n try {\n const parsed: unknown = JSON.parse(raw);\n return isRunnerRecord(parsed) ? parsed : undefined;\n } catch {\n return undefined; // Mid-write or foreign file; the next scan settles it.\n }\n}\n\nexport function toRunnerRunView(record: RunnerRecord): RunnerRunView {\n return {\n id: record.id,\n name: record.name,\n pid: record.pid,\n command: record.command,\n cwd: record.cwd,\n interactive: record.interactive,\n backend: record.backend,\n state: record.state,\n promoted: record.promoted,\n startedAt: record.startedAt,\n logPath: record.logPath,\n ...(record.exit\n ? {\n exit: {\n reason: record.exit.reason,\n code: record.exit.code,\n signal: record.exit.signal,\n ...(record.exit.terminationReason === undefined\n ? {}\n : { terminationReason: record.exit.terminationReason }),\n ...(record.exit.stopReason === undefined ? {} : { stopReason: record.exit.stopReason }),\n finishedAt: record.exit.finishedAt,\n },\n }\n : {}),\n };\n}\n\nfunction finishedAt(record: RunnerRecord): number {\n const parsed = record.exit === undefined ? Number.NaN : Date.parse(record.exit.finishedAt);\n return Number.isFinite(parsed) ? parsed : Date.parse(record.startedAt);\n}\n\n/**\n * The feed the cockpit shows: running runners first, newest start first, then\n * the recently finished ones, newest exit first. Finished runners older than\n * the retention window are left out; the registry keeps them for the CLI.\n */\nexport function presentRunnerRuns(records: readonly RunnerRecord[], now: number): RunnerRunView[] {\n const running = records\n .filter((record) => record.state === 'running')\n .sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n const finished = records\n .filter((record) => record.state === 'completed' && now - finishedAt(record) <= FINISHED_RUNNER_RETENTION_MS)\n .sort((left, right) => finishedAt(right) - finishedAt(left));\n return [...running, ...finished].map(toRunnerRunView);\n}\n","import {\n DOOM_BACKGROUND_WORK_SERVICE,\n readDoomBackgroundWorkService,\n type BackgroundProviderWorkItem,\n type BackgroundWorkProviderHandle,\n} from '@agimon-ai/doompi-core/backgroundWork';\nimport type { DoomHeadlessActivity, DoomHeadlessExecutionContext } from '@agimon-ai/doompi-core/headless';\nimport type { DoomServerHostService } from '@agimon-ai/doompi-core/serverFacet';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { BACKGROUND_WORK_PROVIDER } from '../../constants/runnerRuntime';\nimport { RUNNER_RUNS_TYPE } from '../../constants/webRunners';\nimport { reconcileActiveRunners, stopRunnerProcess } from '../reconcile';\nimport type { RunnerDependencies } from '../runnerDependencies/type';\nimport { formatRunnerFinished } from '../runnerRecord';\nimport { presentRunnerRuns } from '../webRunnerRuns';\n\nconst COMPLETION_RETRY_MS = 1_000;\n\nexport interface RunnerServerRuntime {\n readonly container: RunnerDependencies;\n readonly activity: DoomHeadlessActivity;\n readonly backgroundWorkPlugin: (context: Context) => void;\n ensureSession(sessionId: string): Promise<void>;\n /** Wakes the agent once when this runner, which it sent to the background, exits. */\n watchRunner(id: string): void;\n dispose(this: void): Promise<void>;\n}\nexport function createRunnerServerRuntime(\n container: RunnerDependencies,\n directEvents: NonNullable<DoomServerHostService['context']['directEvents']>,\n /** Absent when no agent runs in this session, so there is nobody to wake. */\n wakeAgent?: (content: string, requestId: string) => Promise<void>,\n): RunnerServerRuntime {\n let sessionId: string | undefined;\n let supervision: Promise<void> | undefined;\n let runtimeDisposal: Promise<void> | undefined;\n let backgroundWorkItems: BackgroundProviderWorkItem[] = [];\n let backgroundWorkProvider: BackgroundWorkProviderHandle | undefined;\n // Nothing else tells the model a background runner ended, so without this an\n // agent that finished its turn waiting on one idles until a person prompts it.\n const watched = new Set<string>();\n let unsubscribeWatched: (() => void) | undefined;\n let wakes: Promise<void> = Promise.resolve();\n let closing = false;\n let retryTimer: ReturnType<typeof setTimeout> | undefined;\n\n // The activity is optional and source-gated. The retained facet owns the\n // session runtime, so disabling the activity must not tear down its jobs.\n const ensureSession = (requestedSessionId: string): Promise<void> => {\n if (closing) return Promise.reject(new Error('Runner runtime is closing.'));\n if (sessionId !== undefined && sessionId !== requestedSessionId) {\n throw new Error(`Runner runtime belongs to session ${sessionId}.`);\n }\n sessionId ??= requestedSessionId;\n supervision ??= (async () => {\n container.paths.setSessionId(requestedSessionId);\n if ((await container.lifeline.arm(requestedSessionId)) === undefined) {\n throw new Error('Could not arm runner supervision lifeline.');\n }\n const reconciled = await reconcileActiveRunners({\n registry: container.runnerRegistry,\n launcher: container.launcher,\n rmuxBackend: container.rmuxBackend,\n processControl: container.processControl,\n currentHostPid: process.pid,\n startup: false,\n active: await container.runnerRegistry.listBySession(requestedSessionId),\n });\n for (const error of reconciled.errors) process.emitWarning(error);\n if (!closing) {\n unsubscribeRunnerUpdates ??= container.runnerRegistry.subscribe(\n () => requestPublish(requestedSessionId),\n requestedSessionId,\n );\n requestPublish(requestedSessionId);\n }\n })().catch((error: unknown) => {\n supervision = undefined;\n throw error;\n });\n return supervision;\n };\n\n const disposeRuntime = (): Promise<void> => {\n runtimeDisposal ??= (async () => {\n await supervision?.catch((error: unknown) =>\n process.emitWarning(`Could not initialize headless runner supervision: ${String(error)}`),\n );\n const ownedSessionId = sessionId;\n if (ownedSessionId === undefined) return;\n const records = await container.runnerRegistry.listBySession(ownedSessionId).catch(() => []);\n await Promise.all(\n records.map(async (record) => {\n try {\n const stopped = await stopRunnerProcess(record, container.launcher, container.rmuxBackend, {\n registry: container.runnerRegistry,\n intent: {\n reason: 'stopped',\n terminationReason: 'parent_session_cleanup',\n stopReason: 'session ended',\n },\n });\n if (!stopped && container.processControl.isAlive(record.pid)) {\n process.emitWarning(`Could not stop runner ${record.id} during session shutdown`);\n return;\n }\n await container.runnerRegistry.complete(\n record.id,\n { reason: 'stopped', code: null, signal: null, stopReason: 'session ended' },\n ownedSessionId,\n );\n } catch (error) {\n process.emitWarning(`Could not clean up runner ${record.id}: ${String(error)}`);\n }\n }),\n );\n try {\n await container.ptyHost.disposeAll();\n } finally {\n container.lifeline.dispose();\n container.runnerRegistry.close();\n }\n })();\n return runtimeDisposal;\n };\n\n const publishRuns = async (ownedSessionId: string): Promise<void> => {\n try {\n const records = await container.runnerRegistry.listAll(ownedSessionId);\n const nextBackgroundWork = records\n .filter((record) => record.state === 'running' || watched.has(record.id))\n .map((record) => ({\n id: record.id,\n sessionId: record.sessionId,\n label: record.name,\n // An absent status holds ownership until completion admission succeeds.\n ...(record.state === 'running' ? { status: record.state } : {}),\n }));\n if (JSON.stringify(nextBackgroundWork) !== JSON.stringify(backgroundWorkItems)) {\n backgroundWorkItems = nextBackgroundWork;\n backgroundWorkProvider?.update();\n }\n directEvents.publish(RUNNER_RUNS_TYPE, ownedSessionId, {\n runs: presentRunnerRuns(records, Date.now()),\n });\n } catch (error) {\n process.emitWarning(`Could not publish runner state: ${String(error)}`);\n }\n };\n let publishInFlight: Promise<void> | undefined;\n let publishAgain = false;\n let unsubscribeRunnerUpdates: (() => void) | undefined;\n const requestPublish = (ownedSessionId: string): void => {\n if (closing) return;\n publishAgain = true;\n if (publishInFlight !== undefined) return;\n publishInFlight = (async () => {\n do {\n publishAgain = false;\n await publishRuns(ownedSessionId);\n } while (publishAgain && !closing);\n })()\n .catch((error: unknown) => {\n process.emitWarning(`Could not schedule runner state publication: ${String(error)}`);\n })\n .finally(() => {\n publishInFlight = undefined;\n if (publishAgain) requestPublish(ownedSessionId);\n });\n };\n\n // Sequenced so two runners exiting in the same tick each wake the agent once.\n const wakeForFinished = (ownedSessionId: string): void => {\n if (closing) return;\n wakes = wakes\n .then(async () => {\n if (closing) return;\n let retry = false;\n // Deleting the entry being visited is safe for a Set iterator.\n for (const id of watched) {\n if (closing) return;\n try {\n const record = await container.runnerRegistry.get(id, ownedSessionId);\n if (closing) return;\n if (record?.state === 'running') continue;\n // A record swept before it was seen leaves nothing to report.\n if (record)\n await wakeAgent?.(\n formatRunnerFinished(record),\n `runner-finished:${JSON.stringify([ownedSessionId, id])}`,\n );\n watched.delete(id);\n requestPublish(ownedSessionId);\n } catch (error) {\n retry = true;\n process.emitWarning(`Could not report finished runner ${id}: ${String(error)}`);\n }\n }\n if (retry && !closing && retryTimer === undefined) {\n retryTimer = setTimeout(() => {\n retryTimer = undefined;\n wakeForFinished(ownedSessionId);\n }, COMPLETION_RETRY_MS);\n retryTimer.unref?.();\n }\n if (watched.size === 0) {\n clearTimeout(retryTimer);\n retryTimer = undefined;\n unsubscribeWatched?.();\n unsubscribeWatched = undefined;\n }\n })\n .catch((error: unknown) => {\n process.emitWarning(`Could not report a finished runner: ${String(error)}`);\n });\n };\n\n const watchRunner = (id: string): void => {\n const ownedSessionId = sessionId;\n if (!wakeAgent || closing || ownedSessionId === undefined) return;\n watched.add(id);\n requestPublish(ownedSessionId);\n // Independent of the optional activity: the wake-up must not depend on a\n // browser watching the runner list.\n unsubscribeWatched ??= container.runnerRegistry.subscribe(() => wakeForFinished(ownedSessionId), ownedSessionId);\n // It may already have exited between promotion and subscription.\n wakeForFinished(ownedSessionId);\n };\n\n const activity: DoomHeadlessActivity = {\n name: 'runner',\n start: async (executionContext: DoomHeadlessExecutionContext) => {\n await ensureSession(executionContext.sessionId);\n requestPublish(executionContext.sessionId);\n return () => {\n // Let the next optional activation reconcile again, while keeping\n // the retained runtime and all child job supervisors alive.\n supervision = undefined;\n };\n },\n };\n return {\n container,\n activity,\n ensureSession,\n watchRunner,\n backgroundWorkPlugin: (context) => {\n context.inject([DOOM_BACKGROUND_WORK_SERVICE], (serviceContext) => {\n const service = readDoomBackgroundWorkService(serviceContext);\n if (!service) return undefined;\n const registration = service.register({\n provider: BACKGROUND_WORK_PROVIDER,\n listActiveWork: () => backgroundWorkItems,\n });\n backgroundWorkProvider = registration;\n return () => {\n registration.dispose();\n if (backgroundWorkProvider === registration) backgroundWorkProvider = undefined;\n };\n });\n },\n async dispose() {\n // Session cleanup completes every runner it stops, and none of those may\n // start a turn in a session that is going away.\n closing = true;\n clearTimeout(retryTimer);\n retryTimer = undefined;\n watched.clear();\n unsubscribeWatched?.();\n unsubscribeWatched = undefined;\n unsubscribeRunnerUpdates?.();\n unsubscribeRunnerUpdates = undefined;\n await wakes;\n await publishInFlight;\n await disposeRuntime();\n },\n };\n}\n","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { createHeadlessBashTool, createHeadlessRunnersCommand } from '../../../../../services/headless';\nimport { summarizeLog } from '../../../../../services/logReader';\nimport { createRunnerDependencies } from '../../../../../services/runnerDependencies';\nimport { createRunnerServerRuntime } from '../../../../../services/runnerServerRuntime';\n\nexport const RUNNER_SERVER_SCOPE_SERVICE = 'doom/runner-server-scope';\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n 'doom/runner-server-scope': RunnerServerScope;\n }\n}\n\nexport const createRunnerServerRoot = ({ host, agent, signal }: DoomServerPluginContext) => {\n if (!host.context.directEvents) throw new Error('Runner headless facet requires the session direct event bus.');\n if (!host.context.cwd || !host.context.environment) {\n throw new Error('Runner headless facet requires session cwd and environment.');\n }\n const dependencies = createRunnerDependencies({\n cwd: host.context.cwd,\n environment: host.context.environment,\n ...(host.context.executionBudget === undefined ? {} : { executionBudget: host.context.executionBudget }),\n });\n const execution = agent?.context;\n const wakeAgent =\n execution &&\n (async (content: string, requestId: string): Promise<void> => {\n try {\n // 'steer' joins a running turn and starts one on an idle session.\n // Reuse completion identity across retries and acknowledge admission, not settlement.\n if (!execution.session.admitPrompt) throw new Error('The session cannot admit a runner notification.');\n await execution.session.admitPrompt(content, 'steer', undefined, requestId);\n } catch (error) {\n // Keep completion ownership with the watcher until admission succeeds.\n await Promise.resolve()\n .then(() => execution.client.notify({ body: String(error), level: 'warning' }))\n .catch(() => undefined);\n throw error;\n }\n });\n const runtime = createRunnerServerRuntime(dependencies, host.context.directEvents, wakeAgent);\n const bash = (wakeOnExit: boolean) =>\n createHeadlessBashTool(\n {\n run: async (request) => {\n await runtime.ensureSession(request.sessionId);\n const result = await dependencies.bashRunService.run({\n ...request,\n ...(signal ? { signal: request.signal ? AbortSignal.any([signal, request.signal]) : signal } : {}),\n });\n // Without this, nothing tells the agent the runner ended.\n if (wakeOnExit && result.kind === 'promoted') runtime.watchRunner(result.id);\n return result;\n },\n },\n summarizeLog,\n );\n const value = {\n tool: bash(true),\n // Remote MCP runners must not wake the local agent; the remote caller polls instead.\n remoteTool: bash(false),\n command: createHeadlessRunnersCommand(dependencies),\n };\n return {\n value,\n services: [\n runtime.backgroundWorkPlugin,\n (context: Context) => {\n context.plugin((providerContext) => providerContext.provide(RUNNER_SERVER_SCOPE_SERVICE, value));\n },\n ],\n activities: [runtime.activity],\n onStop: runtime.dispose,\n };\n};\n\nexport type RunnerServerScope = Awaited<ReturnType<typeof createRunnerServerRoot>>['value'];\n","import { defineRoot } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { createRunnerServerRoot } from './_lib/serverRoot';\n\nexport default defineRoot(createRunnerServerRoot);\n","import type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { RUNNER_RUNS_TYPE } from '../../constants/webRunners';\nimport { type RunnerRunView } from '../../types/webRunners';\n\nfunction runnerPayload(value: unknown): value is { runs: RunnerRunView[] } {\n return typeof value === 'object' && value !== null && Array.isArray((value as { runs?: unknown }).runs);\n}\n\n/**\n * The runners data channel consumes lifecycle-owned snapshots from the session\n * server. Durable registry files are read by the owning session only, never by\n * this hub channel.\n */\nexport function createRunnersChannel(): DoomHubChannel {\n return {\n frameType: RUNNER_RUNS_TYPE,\n start(host) {\n const latest = new Map<string, RunnerRunView[]>();\n const subscriptions = new Map<string, () => void>();\n const source: DoomHubChannelSource = {\n payloadFor(scope) {\n const runs = latest.get(scope.sessionId);\n return runs === undefined ? undefined : { runs };\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n latest.delete(scope.sessionId);\n const unsubscribe = host.directEvents.subscribe(\n RUNNER_RUNS_TYPE,\n scope.sessionId,\n (payload) => {\n if (!runnerPayload(payload)) return;\n latest.set(scope.sessionId, payload.runs);\n host.publish(scope.sessionId, payload);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n };\n return source;\n },\n };\n}\n","import { defineChannel } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomHubChannel } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { createRunnersChannel } from '../../../services/runnersChannel';\nexport default defineChannel(function runnerRunsChannel(): DoomHubChannel {\n return createRunnersChannel();\n});\n","export const LOG_DIR_NAME = 'logs';\nexport const STATE_EXTENSION = '.json';\nexport const RUNNING_STATE = 'running';\nexport const STATE_POLL_MS = 1000;\nexport const HEARTBEAT_MS = 15_000;\nexport const DEFAULT_LINES = 200;\nexport const MAX_LINES = 5000;\nexport const SAFE_SEGMENT = /^[\\w.@-]+$/;\nexport const SCREEN_POLL_MS = 100;\nexport const RAW_LOG_SUFFIX = '.raw';\nexport const MAX_INPUT_CHARS = 4096;\nexport const NOT_ATTACHABLE = 'No attachable runner by that name in this session.';\n","export const RUNNER_API_BASE_PATH = 'runner';\nexport const RUN_ID_PARAM = 'runId';\nexport const RUNNER_SCREEN_EVENT = 'screen';\nexport const RUNNER_LOG_STREAM_EVENT = 'append';\nexport const RUNNER_LOG_PING_EVENT = 'ping';\n","import fs from 'node:fs';\n\nimport { POLL_MS } from '../../constants/logTail';\nimport type { ILogTail, LogTailHandle, LogTailOptions } from '../../types/logTail';\n\n/** How often the tail restats the file; the same cadence the CLI's --follow uses. */\n\n/**\n * Polling tail: read the delta between the last offset and the file's current\n * size, and hand back only complete lines.\n *\n * Same reliability posture as this package's runner watcher: the poll is the\n * mechanism, not an accelerator for a change notification. fs.watchFile only\n * fires when it observes a difference from a baseline it took itself, so a\n * write that lands before its first stat is a change it never reports; a\n * restat every tick has no such window.\n *\n * A write that lands mid-line is common, so the trailing fragment is held\n * until its newline arrives rather than emitted as a line that will change.\n */\nexport class LogTail implements ILogTail {\n follow(logPath: string, options: LogTailOptions): LogTailHandle {\n let offset = options.from;\n let pending = '';\n let closed = false;\n\n const drain = (size: number): void => {\n // A shrunken file was rotated or truncated; start over rather than\n // reading from an offset that now points into different content.\n if (size < offset) {\n offset = 0;\n pending = '';\n }\n if (size === offset) return;\n const length = size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = fs.openSync(logPath, 'r');\n let bytesRead: number;\n try {\n bytesRead = fs.readSync(descriptor, buffer, 0, length, offset);\n } finally {\n fs.closeSync(descriptor);\n }\n offset = size;\n if (bytesRead <= 0) return;\n pending += buffer.subarray(0, bytesRead).toString('utf8');\n const lines = pending.split('\\n');\n pending = lines.pop() ?? '';\n if (lines.length > 0) options.onLines(lines, offset - Buffer.byteLength(pending, 'utf8'));\n };\n\n const tick = (): void => {\n if (closed) return;\n try {\n drain(fs.statSync(logPath).size);\n } catch (error) {\n // The runner has not opened its log yet; the next tick settles it.\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;\n options.onError(error instanceof Error ? error : new Error(String(error)));\n }\n };\n\n const timer = setInterval(tick, POLL_MS);\n // Following a log must never be the reason a process stays alive.\n timer.unref?.();\n return {\n close() {\n if (closed) return;\n closed = true;\n clearInterval(timer);\n },\n };\n }\n}\n","import type { LogSlice } from './logReader';\n\n/**\n * The runner log API, shared by this package's session-scoped routes and its\n * cockpit plugin. The two halves run in different processes, so the wire\n * vocabulary is declared here: the browser half may reach `src/types` and\n * nothing else on the server side.\n *\n * Where the routes live is not here. That is `src/types/apiRoutes`, which both\n * halves read, and the generated client turns into the URLs the page addresses.\n * The file a run writes to is still read from its metadata record by the server\n * alone.\n */\n\n/**\n * One chunk of an attached pane's output.\n *\n * Base64 because these are terminal bytes, escape sequences and all, and a\n * server-sent event is a line-oriented text frame that would mangle them.\n */\nexport interface RunnerScreenEvent {\n chunk: string;\n /** Byte offset just past this chunk, so a reconnect resumes rather than replays. */\n offset?: number;\n /** Set once the runner exits, so the page can stop watching. */\n ended?: boolean;\n}\n\n/** The body of a keystroke POST. Text, not key codes: the pane wants bytes. */\nexport interface RunnerInputRequest {\n text: string;\n}\n\n/** The named SSE event carrying a RunnerScreenEvent payload. */\n\n/**\n * A log request as query parameters. `grep` is a literal substring, not a\n * regular expression, because that is what the reader matches; `lines` bounds\n * the answer to the last N lines of whatever survived the filter.\n */\nexport interface RunnerLogQueryParams {\n lines?: number;\n grep?: string;\n ignoreCase?: boolean;\n contextLines?: number;\n}\n\n/** The slice the log route answers with, plus which runner it came from. */\nexport interface RunnerLogResponse extends LogSlice {\n runId: string;\n /** True while the runner is still writing, so the page knows following is worth offering. */\n running: boolean;\n}\n\n/**\n * One server-sent event on the follow stream: the lines appended since the last\n * one. The stream never filters: a page that sets a query stops following and\n * reads a filtered slice instead, because grep with context is a whole-file\n * answer and cannot be assembled from a chunk at a time.\n */\nexport interface RunnerLogStreamEvent {\n /** Appended lines, in order; empty when the event only reports the runner ending. */\n lines: string[];\n /**\n * Byte offset just past the last line in this event.\n *\n * A follower that loses the stream resumes here. Without it the page could\n * only fall back to the offset its last slice reported, which every line\n * delivered since has already moved past, so a reconnect would replay them.\n */\n offset?: number;\n /** Set once the runner exits, so the page can stop following without polling the run list. */\n ended?: boolean;\n}\n\n/** The named SSE event carrying a RunnerLogStreamEvent payload. */\n\n/**\n * The named SSE event that carries nothing.\n *\n * A runner can be alive and silent for minutes, which on the wire is\n * indistinguishable from a connection that died. This is sent on a timer so\n * the socket keeps proving itself. Readers that only listen for `append`\n * ignore it, which is what makes it safe to add.\n */\n\n/** Query parameter names, shared so the page and the route cannot drift apart. */\nexport const RUNNER_LOG_PARAMS = {\n lines: 'lines',\n grep: 'grep',\n ignoreCase: 'ignoreCase',\n contextLines: 'contextLines',\n from: 'from',\n} as const;\n","import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport { RUN_ID_PARAM } from '../constants/webRunnerLog';\nimport { RUNNER_LOG_PARAMS, type RunnerLogResponse } from './webRunnerLog';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path: the build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/runner/`, so the mount is\n * stated once, by the folder that creates it. This package stated it four times\n * before, once per URL builder, each of them re-spelling `/plugins/` and the\n * session prefix by hand.\n *\n * The Hono app registers these paths verbatim and the browser addresses them,\n * so a path cannot move on one side alone. `:runId` is Hono's own parameter\n * syntax for that reason; a caller supplies the value as `params`, and the\n * client encodes the segment.\n */\nexport default defineApiRoutes({\n log: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/log`,\n query: [\n RUNNER_LOG_PARAMS.lines,\n RUNNER_LOG_PARAMS.grep,\n RUNNER_LOG_PARAMS.ignoreCase,\n RUNNER_LOG_PARAMS.contextLines,\n ],\n response: apiResponse<RunnerLogResponse>(),\n },\n /**\n * The live tail of that same log.\n *\n * `stream: true`, so the client exposes a URL and no call method: the\n * consumer is the browser's own `EventSource`, and a relayed call would\n * buffer a stream that never ends.\n */\n logStream: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/log/stream`,\n query: [RUNNER_LOG_PARAMS.from],\n stream: true,\n },\n /** The unscrubbed pane bytes, for a terminal emulator rather than a reader. */\n screenStream: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/screen/stream`,\n query: [RUNNER_LOG_PARAMS.from],\n stream: true,\n },\n input: {\n method: 'POST',\n path: `/runners/:${RUN_ID_PARAM}/screen/input`,\n },\n});\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/packageApi';\nimport { Hono } from 'hono';\nimport { streamSSE } from 'hono/streaming';\n\nimport {\n DEFAULT_LINES,\n HEARTBEAT_MS,\n LOG_DIR_NAME,\n MAX_INPUT_CHARS,\n MAX_LINES,\n NOT_ATTACHABLE,\n RAW_LOG_SUFFIX,\n RUNNING_STATE,\n SAFE_SEGMENT,\n SCREEN_POLL_MS,\n STATE_EXTENSION,\n STATE_POLL_MS,\n} from '../../constants/runnerLogApi';\nimport {\n RUN_ID_PARAM,\n RUNNER_API_BASE_PATH,\n RUNNER_LOG_PING_EVENT,\n RUNNER_LOG_STREAM_EVENT,\n RUNNER_SCREEN_EVENT,\n} from '../../constants/webRunnerLog';\nimport { LogReader } from '../../services/logReader';\nimport { LogTail } from '../../services/logTail';\nimport { PtyBackendChain } from '../../services/ptyBackendChain';\nimport { RmuxBackend } from '../../services/rmuxBackend';\nimport { RunnerPaths, resolveRunnerStoreDirectory, runnerStateDirFor } from '../../services/runnerPaths';\nimport { TmuxBackend } from '../../services/tmuxBackend';\nimport { parseRunnerRecord } from '../../services/webRunnerRuns';\nimport routes from '../../types/apiRoutes';\nimport type { ILogReader, LogQuery } from '../../types/logReader';\nimport type { ILogTail } from '../../types/logTail';\nimport type { IRmuxBackend } from '../../types/rmuxBackend';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport {\n RUNNER_LOG_PARAMS,\n type RunnerLogResponse,\n type RunnerLogStreamEvent,\n type RunnerScreenEvent,\n} from '../../types/webRunnerLog';\n\n/** How often a follow re-reads the record to notice the runner exiting. */\n/**\n * How often a silent but living stream proves it is still there. Short enough\n * that an idle proxy does not reap the socket, long enough to cost nothing.\n */\n/** Kept in step with LogQuery's own default, so an unasked-for tail is the same size everywhere. */\n/** A page that asks for more than this gets this; the whole point of the route is a bounded read. */\n/** Path segments the client supplies are names, never paths. */\n/** Backends that expose a pane another process can read and write. */\nconst MULTIPLEXER_BACKENDS: ReadonlySet<string> = new Set(['rmux', 'tmux']);\n/** How often an attached pane's new bytes are picked up. */\n/** The unscrubbed copy the runner's sink writes beside the log, for interactive runs. */\n/** One keystroke batch; a paste is not a file transfer. */\n/** Said the same way for every reason, so the route never reports which check failed. */\n\nexport interface RunnerLogApiOptions {\n /** The session these routes answer for; the host owns exactly one. */\n sessionId: string;\n /** The store root; defaults to this environment's agent directory. */\n storeDir?: string;\n environment?: Readonly<Record<string, string | undefined>>;\n cwd?: string;\n logReader?: ILogReader;\n logTail?: ILogTail;\n /**\n * Reaches a live pane to read its screen and send it input.\n *\n * A session API is handed nothing but its session id, so it cannot touch the\n * in-process PTY handles the extension holds. This talks to the multiplexer\n * over its own socket instead, the same way the CLI's `input` verb does,\n * which is what makes an attached pane reachable across processes at all.\n */\n pane?: IRmuxBackend;\n}\n\n/** Rejects anything that could climb out of the session's own directory before a path is built. */\nfunction isSafeSegment(value: string): boolean {\n return value !== '' && value !== '.' && value !== '..' && SAFE_SEGMENT.test(value);\n}\n\n/**\n * The log file a run wrote, or undefined when there is no such run or its\n * record points somewhere it has no business pointing.\n *\n * The record names its own log path, and that record is written by another\n * process, so the path is checked rather than trusted: it must sit inside this\n * session's own logs directory. Without that check a doctored record would\n * turn this route into an arbitrary file read.\n */\nfunction resolveLogFile(stateDir: string, logsDir: string, sessionId: string, runId: string): RunnerRecord | undefined {\n const statePath = path.join(stateDir, `${runId}${STATE_EXTENSION}`);\n let raw: string;\n try {\n raw = fs.readFileSync(statePath, 'utf8');\n } catch {\n return undefined; // No such run in this session; the route answers 404.\n }\n const record = parseRunnerRecord(raw);\n if (record === undefined || record.id !== runId || record.sessionId !== sessionId) return undefined;\n const resolved = path.resolve(record.logPath);\n if (resolved !== logsDir && !resolved.startsWith(`${logsDir}${path.sep}`)) return undefined;\n return record;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction numberParam(value: string | undefined, fallback: number, max: number): number {\n const parsed = value === undefined ? Number.NaN : Number.parseInt(value, 10);\n if (!Number.isFinite(parsed) || parsed < 0) return fallback;\n return Math.min(parsed, max);\n}\n\nfunction logQueryOf(url: URL): LogQuery {\n const grep = url.searchParams.get(RUNNER_LOG_PARAMS.grep) ?? undefined;\n return {\n lines: numberParam(url.searchParams.get(RUNNER_LOG_PARAMS.lines) ?? undefined, DEFAULT_LINES, MAX_LINES),\n ...(grep === undefined || grep === '' ? {} : { grep }),\n ignoreCase: url.searchParams.get(RUNNER_LOG_PARAMS.ignoreCase) === 'true',\n contextLines: numberParam(url.searchParams.get(RUNNER_LOG_PARAMS.contextLines) ?? undefined, 0, MAX_LINES),\n };\n}\n\n/**\n * This package's HTTP surface: one runner's log, as a bounded slice and as a\n * live tail.\n *\n * The routes are mounted inside one session's own server, so they name a runner\n * and nothing else; the session is the host, not a parameter. The reader only\n * ever returns the last N lines of what matched, so both routes stay bounded no\n * matter how large the file on disk is.\n *\n * Paths come from src/types/apiRoutes, the one table the browser's generated\n * client reads too, so this app and the cockpit cannot disagree about where a\n * route lives. They stay relative to the mount the host strips.\n */\nexport function createRunnerLogApi(options: RunnerLogApiOptions): Hono {\n const logReader = options.logReader ?? new LogReader();\n const logTail = options.logTail ?? new LogTail();\n const sessionId = options.sessionId;\n const paths = new RunnerPaths(options.cwd, options.environment);\n const storeDir = options.storeDir ?? resolveRunnerStoreDirectory(options.environment ?? process.env, options.cwd);\n const stateDir =\n options.storeDir === undefined ? paths.stateDirectory(sessionId) : runnerStateDirFor(storeDir, sessionId);\n const logsDir =\n options.storeDir === undefined ? paths.logDirectory(sessionId) : path.join(storeDir, sessionId, LOG_DIR_NAME);\n const app = new Hono();\n\n // Built on first use, not on mount: most sessions never attach to a pane,\n // and constructing this probes the filesystem for a multiplexer binary.\n let paneBackend: IRmuxBackend | undefined = options.pane;\n const pane = (): IRmuxBackend => {\n paneBackend ??= (() => {\n const paths = new RunnerPaths(options.cwd, options.environment);\n return new PtyBackendChain(new RmuxBackend(paths), new TmuxBackend(paths));\n })();\n return paneBackend;\n };\n\n /** The record behind a request, or undefined when this session has no such run. */\n const runnerOf = (runId: string): RunnerRecord | undefined =>\n isSafeSegment(runId) ? resolveLogFile(stateDir, logsDir, sessionId, runId) : undefined;\n\n /**\n * The pane a request may attach to.\n *\n * Same posture as the log routes: the record is written by another process,\n * so nothing on it is trusted until it is checked. The target must be one\n * this package owns, the run must still be up, and it must have been started\n * interactive. A non-interactive run has no terminal to type into, and a\n * subprocess-backed run has no pane at all.\n */\n const attachableOf = (runId: string): { record: RunnerRecord; target: string } | undefined => {\n const record = runnerOf(runId);\n if (record === undefined || record.state !== RUNNING_STATE || record.interactive !== true) return undefined;\n const target = record.backendTarget;\n if (target === undefined || !MULTIPLEXER_BACKENDS.has(record.backend) || !isSafeSegment(target)) return undefined;\n return { record, target };\n };\n app.get(routes.log.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const record = runnerOf(runId);\n if (record === undefined) return context.json({ error: `No runner '${runId}' in this session.` }, 404);\n const slice = logReader.read(record.logPath, logQueryOf(new URL(context.req.url)));\n const body: RunnerLogResponse = { ...slice, runId, running: record.state === RUNNING_STATE };\n return context.json(body);\n });\n\n app.get(routes.logStream.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const record = runnerOf(runId);\n if (record === undefined) return context.json({ error: `No runner '${runId}' in this session.` }, 404);\n const from = numberParam(\n new URL(context.req.url).searchParams.get(RUNNER_LOG_PARAMS.from) ?? undefined,\n 0,\n Number.MAX_SAFE_INTEGER,\n );\n\n return streamSSE(context, async (stream) => {\n const send = (event: RunnerLogStreamEvent): Promise<void> =>\n stream.writeSSE({ event: RUNNER_LOG_STREAM_EVENT, data: JSON.stringify(event) });\n\n // The tail and the state poll both push onto one queue, so the stream\n // writes in the order things happened rather than interleaving two\n // writers onto the same socket.\n let pump: Promise<void> = Promise.resolve();\n const queue = (event: RunnerLogStreamEvent): void => {\n pump = pump.then(() => send(event)).catch(() => undefined);\n };\n\n let finish = (): void => undefined;\n const done = new Promise<void>((resolve) => {\n finish = resolve;\n });\n const handle = logTail.follow(record.logPath, {\n from,\n onLines: (lines, completeThrough) => queue({ lines, offset: completeThrough }),\n onError: () => finish(),\n });\n // A finished runner writes nothing more, so the page is told once and\n // the stream ends rather than holding a socket open forever.\n const poll = setInterval(() => {\n const current = runnerOf(runId);\n if (current !== undefined && current.state === RUNNING_STATE) return;\n queue({ lines: [], ended: true });\n finish();\n }, STATE_POLL_MS);\n // A runner can be alive and produce nothing for minutes. Without this the\n // page cannot tell that from a stream that quietly died.\n const heartbeat = setInterval(() => {\n pump = pump.then(() => stream.writeSSE({ event: RUNNER_LOG_PING_EVENT, data: '' })).catch(() => undefined);\n }, HEARTBEAT_MS);\n\n stream.onAbort(() => finish());\n await done;\n clearInterval(poll);\n clearInterval(heartbeat);\n handle.close();\n await pump;\n });\n });\n\n app.get(routes.screenStream.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const attachable = attachableOf(runId);\n if (attachable === undefined) return context.json({ error: NOT_ATTACHABLE }, 404);\n // The record's log path is already proven to sit inside this session's own\n // logs directory, and the raw copy is that same path with a suffix, so it\n // cannot point anywhere new.\n const rawPath = `${attachable.record.logPath}${RAW_LOG_SUFFIX}`;\n const from = numberParam(\n new URL(context.req.url).searchParams.get(RUNNER_LOG_PARAMS.from) ?? undefined,\n 0,\n Number.MAX_SAFE_INTEGER,\n );\n\n return streamSSE(context, async (stream) => {\n let pump: Promise<void> = Promise.resolve();\n const queue = (event: RunnerScreenEvent): void => {\n pump = pump\n .then(() => stream.writeSSE({ event: RUNNER_SCREEN_EVENT, data: JSON.stringify(event) }))\n .catch(() => undefined);\n };\n\n let finish = (): void => undefined;\n const done = new Promise<void>((resolve) => {\n finish = resolve;\n });\n\n let offset = from;\n const drain = (): void => {\n let size: number;\n try {\n size = fs.statSync(rawPath).size;\n } catch {\n return; // The sink has not opened the file yet; the next tick settles it.\n }\n // The sink starts the file over when it passes its ceiling, so a\n // smaller file is a fresh one rather than a bad offset.\n if (size < offset) offset = 0;\n if (size === offset) return;\n const length = size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = fs.openSync(rawPath, 'r');\n let read: number;\n try {\n read = fs.readSync(descriptor, buffer, 0, length, offset);\n } finally {\n fs.closeSync(descriptor);\n }\n if (read <= 0) return;\n offset += read;\n // Bytes, not lines: a terminal's escape sequences straddle newlines and\n // mean nothing once split on them.\n queue({ chunk: buffer.subarray(0, read).toString('base64'), offset });\n };\n\n const poll = setInterval(() => {\n const current = runnerOf(runId);\n drain();\n if (current === undefined || current.state !== RUNNING_STATE) {\n queue({ chunk: '', offset, ended: true });\n finish();\n }\n }, SCREEN_POLL_MS);\n\n stream.onAbort(() => finish());\n await done;\n clearInterval(poll);\n await pump;\n });\n });\n\n /**\n * Types into a live pane.\n *\n * This is the one route in the package that writes rather than reads, so it\n * is deliberately narrow: an owned target, still running, started\n * interactive, and a body that is nothing but text.\n */\n app.post(routes.input.path, async (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const attachable = attachableOf(runId);\n if (attachable === undefined) return context.json({ error: NOT_ATTACHABLE }, 404);\n\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'Expected a JSON body carrying text.' }, 400);\n }\n const text = isRecord(body) ? body.text : undefined;\n if (typeof text !== 'string' || text === '') return context.json({ error: 'Expected a non-empty text.' }, 400);\n if (text.length > MAX_INPUT_CHARS) return context.json({ error: 'That is more input than a pane takes.' }, 413);\n\n const delivered = await pane().input(attachable.target, text);\n if (!delivered) return context.json({ error: 'The runner did not take the input.' }, 502);\n return context.json({ delivered: true });\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built session entry. */\nexport const api: DoomApi = {\n basePath: RUNNER_API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n // The host is one session's server, so its id is the whole scope; a hub\n // would have handed no session at all, and there is nothing to answer.\n const app = createRunnerLogApi({\n sessionId: context.sessionId ?? '',\n cwd: context.cwd,\n environment: context.environment,\n });\n return {\n fetch: (request) => app.fetch(request),\n // Nothing outlives a request: a follow's watch and its poll are both\n // torn down when its own stream ends or the socket aborts.\n close: () => undefined,\n };\n },\n};\n","import { defineRoute } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { api } from '../../../../../../services/runnerLogApi';\n\n/**\n * Mounts this package's HTTP API at `runner`, which the folder names.\n *\n * The folder is the declaration: the build reads the base path off it and hands\n * the browser a client bound to the same segment, so the mount is stated once\n * rather than in a constant and in four URL builders that were each free to\n * drift.\n */\nexport default defineRoute(api);\n","import { defineServerCommand, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { RunnerServerScope } from '../_lib/serverRoot';\nexport default defineServerCommand(\n (context: WithRoot<DoomServerPluginContext, RunnerServerScope>) => context.root.command,\n);\n","import { defineServerTool, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { RunnerServerScope } from '../_lib/serverRoot';\nexport default defineServerTool((context: WithRoot<DoomServerPluginContext, RunnerServerScope>) => context.root.tool);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\nimport { composeRootHooks } from '@agimon-ai/doompi-core/extensionFile';\n\nimport rootSession from '../src/extensions/workspaces/sessions/(backend)/root.server';\nimport channelRunnerRuns from '../src/extensions/(backend)/channel/runnerRuns.server';\nimport apiRoute from '../src/extensions/workspaces/sessions/(backend)/api/runner/route.server';\nimport commandRunners from '../src/extensions/workspaces/sessions/(backend)/command/runners.server';\nimport toolBash from '../src/extensions/workspaces/sessions/(backend)/tool/bash.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/doompi-runner',\n global: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n }),\n workspace: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n }),\n session: async (context) => {\n const scopeSession = await rootSession(context);\n const contextSession = { ...context, root: scopeSession.value };\n return {\n ...composeRootHooks(scopeSession),\n get api(): DoomServerSessionPlugin['api'] { return [at(apiRoute, contextSession)]; },\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n get commands(): DoomServerSessionPlugin['commands'] { return [via({ name: 'runners' }, at(commandRunners, contextSession))]; },\n get tools(): DoomServerSessionPlugin['tools'] { return [via({ name: 'bash' }, at(toolBash, contextSession))]; },\n services: [...(scopeSession.services ?? [])],\n get activities(): DoomServerSessionPlugin['activities'] { return [...(scopeSession.activities ?? [])]; },\n };\n },\n});\n\nexport default facet;\n"],"mappings":"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"}
|
|
@@ -147,11 +147,11 @@ function createRunnerServerRuntime(container, directEvents, wakeAgent) {
|
|
|
147
147
|
const publishRuns = async (ownedSessionId) => {
|
|
148
148
|
try {
|
|
149
149
|
const records = await container.runnerRegistry.listAll(ownedSessionId);
|
|
150
|
-
const nextBackgroundWork = records.filter((record) => record.state === "running").map((record) => ({
|
|
150
|
+
const nextBackgroundWork = records.filter((record) => record.state === "running" || watched.has(record.id)).map((record) => ({
|
|
151
151
|
id: record.id,
|
|
152
152
|
sessionId: record.sessionId,
|
|
153
153
|
label: record.name,
|
|
154
|
-
status: record.state
|
|
154
|
+
...record.state === "running" ? { status: record.state } : {}
|
|
155
155
|
}));
|
|
156
156
|
if (JSON.stringify(nextBackgroundWork) !== JSON.stringify(backgroundWorkItems)) {
|
|
157
157
|
backgroundWorkItems = nextBackgroundWork;
|
|
@@ -194,6 +194,7 @@ function createRunnerServerRuntime(container, directEvents, wakeAgent) {
|
|
|
194
194
|
if (record?.state === "running") continue;
|
|
195
195
|
if (record) await wakeAgent?.(formatRunnerFinished(record), `runner-finished:${JSON.stringify([ownedSessionId, id])}`);
|
|
196
196
|
watched.delete(id);
|
|
197
|
+
requestPublish(ownedSessionId);
|
|
197
198
|
} catch (error) {
|
|
198
199
|
retry = true;
|
|
199
200
|
process.emitWarning(`Could not report finished runner ${id}: ${String(error)}`);
|
|
@@ -220,6 +221,7 @@ function createRunnerServerRuntime(container, directEvents, wakeAgent) {
|
|
|
220
221
|
const ownedSessionId = sessionId;
|
|
221
222
|
if (!wakeAgent || closing || ownedSessionId === void 0) return;
|
|
222
223
|
watched.add(id);
|
|
224
|
+
requestPublish(ownedSessionId);
|
|
223
225
|
unsubscribeWatched ??= container.runnerRegistry.subscribe(() => wakeForFinished(ownedSessionId), ownedSessionId);
|
|
224
226
|
wakeForFinished(ownedSessionId);
|
|
225
227
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":["routes","channelRunnerRuns","rootSession","apiRoute","commandRunners","toolBash"],"sources":["../../src/constants/webRunners.ts","../../src/constants/webRunnerRuns.ts","../../src/services/webRunnerRuns/index.ts","../../src/services/runnerServerRuntime/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/root.server.ts","../../src/services/runnersChannel/index.ts","../../src/extensions/(backend)/channel/runnerRuns.server.ts","../../src/constants/runnerLogApi.ts","../../src/constants/webRunnerLog.ts","../../src/services/logTail/index.ts","../../src/types/webRunnerLog.ts","../../src/types/apiRoutes.ts","../../src/services/runnerLogApi/index.ts","../../src/extensions/workspaces/sessions/(backend)/api/runner/route.server.ts","../../src/extensions/workspaces/sessions/(backend)/command/runners.server.ts","../../src/extensions/workspaces/sessions/(backend)/tool/bash.server.ts","../../generated/server.ts"],"sourcesContent":["export const RUNNER_RUNS_TYPE = 'runner_runs';\n","export const FINISHED_RUNNER_RETENTION_MS = 600_000;\n","import { FINISHED_RUNNER_RETENTION_MS } from '../../constants/webRunnerRuns';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport type { RunnerRunView } from '../../types/webRunners';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** How long a finished runner stays in the cockpit's feed after it exited. */\n\n/** One on-disk metadata record, or undefined when the text is not a runner record. */\nexport function parseRunnerRecord(raw: string): RunnerRecord | undefined {\n try {\n const parsed: unknown = JSON.parse(raw);\n return isRunnerRecord(parsed) ? parsed : undefined;\n } catch {\n return undefined; // Mid-write or foreign file; the next scan settles it.\n }\n}\n\nexport function toRunnerRunView(record: RunnerRecord): RunnerRunView {\n return {\n id: record.id,\n name: record.name,\n pid: record.pid,\n command: record.command,\n cwd: record.cwd,\n interactive: record.interactive,\n backend: record.backend,\n state: record.state,\n promoted: record.promoted,\n startedAt: record.startedAt,\n logPath: record.logPath,\n ...(record.exit\n ? {\n exit: {\n reason: record.exit.reason,\n code: record.exit.code,\n signal: record.exit.signal,\n ...(record.exit.terminationReason === undefined\n ? {}\n : { terminationReason: record.exit.terminationReason }),\n ...(record.exit.stopReason === undefined ? {} : { stopReason: record.exit.stopReason }),\n finishedAt: record.exit.finishedAt,\n },\n }\n : {}),\n };\n}\n\nfunction finishedAt(record: RunnerRecord): number {\n const parsed = record.exit === undefined ? Number.NaN : Date.parse(record.exit.finishedAt);\n return Number.isFinite(parsed) ? parsed : Date.parse(record.startedAt);\n}\n\n/**\n * The feed the cockpit shows: running runners first, newest start first, then\n * the recently finished ones, newest exit first. Finished runners older than\n * the retention window are left out; the registry keeps them for the CLI.\n */\nexport function presentRunnerRuns(records: readonly RunnerRecord[], now: number): RunnerRunView[] {\n const running = records\n .filter((record) => record.state === 'running')\n .sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n const finished = records\n .filter((record) => record.state === 'completed' && now - finishedAt(record) <= FINISHED_RUNNER_RETENTION_MS)\n .sort((left, right) => finishedAt(right) - finishedAt(left));\n return [...running, ...finished].map(toRunnerRunView);\n}\n","import {\n DOOM_BACKGROUND_WORK_SERVICE,\n readDoomBackgroundWorkService,\n type BackgroundProviderWorkItem,\n type BackgroundWorkProviderHandle,\n} from '@agimon-ai/doompi-core/backgroundWork';\nimport type { DoomHeadlessActivity, DoomHeadlessExecutionContext } from '@agimon-ai/doompi-core/headless';\nimport type { DoomServerHostService } from '@agimon-ai/doompi-core/serverFacet';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { BACKGROUND_WORK_PROVIDER } from '../../constants/runnerRuntime';\nimport { RUNNER_RUNS_TYPE } from '../../constants/webRunners';\nimport { reconcileActiveRunners, stopRunnerProcess } from '../reconcile';\nimport type { RunnerDependencies } from '../runnerDependencies/type';\nimport { formatRunnerFinished } from '../runnerRecord';\nimport { presentRunnerRuns } from '../webRunnerRuns';\n\nconst COMPLETION_RETRY_MS = 1_000;\n\nexport interface RunnerServerRuntime {\n readonly container: RunnerDependencies;\n readonly activity: DoomHeadlessActivity;\n readonly backgroundWorkPlugin: (context: Context) => void;\n ensureSession(sessionId: string): Promise<void>;\n /** Wakes the agent once when this runner, which it sent to the background, exits. */\n watchRunner(id: string): void;\n dispose(this: void): Promise<void>;\n}\nexport function createRunnerServerRuntime(\n container: RunnerDependencies,\n directEvents: NonNullable<DoomServerHostService['context']['directEvents']>,\n /** Absent when no agent runs in this session, so there is nobody to wake. */\n wakeAgent?: (content: string, requestId: string) => Promise<void>,\n): RunnerServerRuntime {\n let sessionId: string | undefined;\n let supervision: Promise<void> | undefined;\n let runtimeDisposal: Promise<void> | undefined;\n let backgroundWorkItems: BackgroundProviderWorkItem[] = [];\n let backgroundWorkProvider: BackgroundWorkProviderHandle | undefined;\n // Nothing else tells the model a background runner ended, so without this an\n // agent that finished its turn waiting on one idles until a person prompts it.\n const watched = new Set<string>();\n let unsubscribeWatched: (() => void) | undefined;\n let wakes: Promise<void> = Promise.resolve();\n let closing = false;\n let retryTimer: ReturnType<typeof setTimeout> | undefined;\n\n // The activity is optional and source-gated. The retained facet owns the\n // session runtime, so disabling the activity must not tear down its jobs.\n const ensureSession = (requestedSessionId: string): Promise<void> => {\n if (closing) return Promise.reject(new Error('Runner runtime is closing.'));\n if (sessionId !== undefined && sessionId !== requestedSessionId) {\n throw new Error(`Runner runtime belongs to session ${sessionId}.`);\n }\n sessionId ??= requestedSessionId;\n supervision ??= (async () => {\n container.paths.setSessionId(requestedSessionId);\n if ((await container.lifeline.arm(requestedSessionId)) === undefined) {\n throw new Error('Could not arm runner supervision lifeline.');\n }\n const reconciled = await reconcileActiveRunners({\n registry: container.runnerRegistry,\n launcher: container.launcher,\n rmuxBackend: container.rmuxBackend,\n processControl: container.processControl,\n currentHostPid: process.pid,\n startup: false,\n active: await container.runnerRegistry.listBySession(requestedSessionId),\n });\n for (const error of reconciled.errors) process.emitWarning(error);\n if (!closing) {\n unsubscribeRunnerUpdates ??= container.runnerRegistry.subscribe(\n () => requestPublish(requestedSessionId),\n requestedSessionId,\n );\n requestPublish(requestedSessionId);\n }\n })().catch((error: unknown) => {\n supervision = undefined;\n throw error;\n });\n return supervision;\n };\n\n const disposeRuntime = (): Promise<void> => {\n runtimeDisposal ??= (async () => {\n await supervision?.catch((error: unknown) =>\n process.emitWarning(`Could not initialize headless runner supervision: ${String(error)}`),\n );\n const ownedSessionId = sessionId;\n if (ownedSessionId === undefined) return;\n const records = await container.runnerRegistry.listBySession(ownedSessionId).catch(() => []);\n await Promise.all(\n records.map(async (record) => {\n try {\n const stopped = await stopRunnerProcess(record, container.launcher, container.rmuxBackend, {\n registry: container.runnerRegistry,\n intent: {\n reason: 'stopped',\n terminationReason: 'parent_session_cleanup',\n stopReason: 'session ended',\n },\n });\n if (!stopped && container.processControl.isAlive(record.pid)) {\n process.emitWarning(`Could not stop runner ${record.id} during session shutdown`);\n return;\n }\n await container.runnerRegistry.complete(\n record.id,\n { reason: 'stopped', code: null, signal: null, stopReason: 'session ended' },\n ownedSessionId,\n );\n } catch (error) {\n process.emitWarning(`Could not clean up runner ${record.id}: ${String(error)}`);\n }\n }),\n );\n try {\n await container.ptyHost.disposeAll();\n } finally {\n container.lifeline.dispose();\n container.runnerRegistry.close();\n }\n })();\n return runtimeDisposal;\n };\n\n const publishRuns = async (ownedSessionId: string): Promise<void> => {\n try {\n const records = await container.runnerRegistry.listAll(ownedSessionId);\n const nextBackgroundWork = records\n .filter((record) => record.state === 'running')\n .map((record) => ({\n id: record.id,\n sessionId: record.sessionId,\n label: record.name,\n status: record.state,\n }));\n if (JSON.stringify(nextBackgroundWork) !== JSON.stringify(backgroundWorkItems)) {\n backgroundWorkItems = nextBackgroundWork;\n backgroundWorkProvider?.update();\n }\n directEvents.publish(RUNNER_RUNS_TYPE, ownedSessionId, {\n runs: presentRunnerRuns(records, Date.now()),\n });\n } catch (error) {\n process.emitWarning(`Could not publish runner state: ${String(error)}`);\n }\n };\n let publishInFlight: Promise<void> | undefined;\n let publishAgain = false;\n let unsubscribeRunnerUpdates: (() => void) | undefined;\n const requestPublish = (ownedSessionId: string): void => {\n if (closing) return;\n publishAgain = true;\n if (publishInFlight !== undefined) return;\n publishInFlight = (async () => {\n do {\n publishAgain = false;\n await publishRuns(ownedSessionId);\n } while (publishAgain && !closing);\n })()\n .catch((error: unknown) => {\n process.emitWarning(`Could not schedule runner state publication: ${String(error)}`);\n })\n .finally(() => {\n publishInFlight = undefined;\n if (publishAgain) requestPublish(ownedSessionId);\n });\n };\n\n // Sequenced so two runners exiting in the same tick each wake the agent once.\n const wakeForFinished = (ownedSessionId: string): void => {\n if (closing) return;\n wakes = wakes\n .then(async () => {\n if (closing) return;\n let retry = false;\n // Deleting the entry being visited is safe for a Set iterator.\n for (const id of watched) {\n if (closing) return;\n try {\n const record = await container.runnerRegistry.get(id, ownedSessionId);\n if (closing) return;\n if (record?.state === 'running') continue;\n // A record swept before it was seen leaves nothing to report.\n if (record)\n await wakeAgent?.(\n formatRunnerFinished(record),\n `runner-finished:${JSON.stringify([ownedSessionId, id])}`,\n );\n watched.delete(id);\n } catch (error) {\n retry = true;\n process.emitWarning(`Could not report finished runner ${id}: ${String(error)}`);\n }\n }\n if (retry && !closing && retryTimer === undefined) {\n retryTimer = setTimeout(() => {\n retryTimer = undefined;\n wakeForFinished(ownedSessionId);\n }, COMPLETION_RETRY_MS);\n retryTimer.unref?.();\n }\n if (watched.size === 0) {\n clearTimeout(retryTimer);\n retryTimer = undefined;\n unsubscribeWatched?.();\n unsubscribeWatched = undefined;\n }\n })\n .catch((error: unknown) => {\n process.emitWarning(`Could not report a finished runner: ${String(error)}`);\n });\n };\n\n const watchRunner = (id: string): void => {\n const ownedSessionId = sessionId;\n if (!wakeAgent || closing || ownedSessionId === undefined) return;\n watched.add(id);\n // Independent of the optional activity: the wake-up must not depend on a\n // browser watching the runner list.\n unsubscribeWatched ??= container.runnerRegistry.subscribe(() => wakeForFinished(ownedSessionId), ownedSessionId);\n // It may already have exited between promotion and subscription.\n wakeForFinished(ownedSessionId);\n };\n\n const activity: DoomHeadlessActivity = {\n name: 'runner',\n start: async (executionContext: DoomHeadlessExecutionContext) => {\n await ensureSession(executionContext.sessionId);\n requestPublish(executionContext.sessionId);\n return () => {\n // Let the next optional activation reconcile again, while keeping\n // the retained runtime and all child job supervisors alive.\n supervision = undefined;\n };\n },\n };\n return {\n container,\n activity,\n ensureSession,\n watchRunner,\n backgroundWorkPlugin: (context) => {\n context.inject([DOOM_BACKGROUND_WORK_SERVICE], (serviceContext) => {\n const service = readDoomBackgroundWorkService(serviceContext);\n if (!service) return undefined;\n const registration = service.register({\n provider: BACKGROUND_WORK_PROVIDER,\n listActiveWork: () => backgroundWorkItems,\n });\n backgroundWorkProvider = registration;\n return () => {\n registration.dispose();\n if (backgroundWorkProvider === registration) backgroundWorkProvider = undefined;\n };\n });\n },\n async dispose() {\n // Session cleanup completes every runner it stops, and none of those may\n // start a turn in a session that is going away.\n closing = true;\n clearTimeout(retryTimer);\n retryTimer = undefined;\n watched.clear();\n unsubscribeWatched?.();\n unsubscribeWatched = undefined;\n unsubscribeRunnerUpdates?.();\n unsubscribeRunnerUpdates = undefined;\n await wakes;\n await publishInFlight;\n await disposeRuntime();\n },\n };\n}\n","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { createHeadlessBashTool, createHeadlessRunnersCommand } from '../../../../../services/headless';\nimport { summarizeLog } from '../../../../../services/logReader';\nimport { createRunnerDependencies } from '../../../../../services/runnerDependencies';\nimport { createRunnerServerRuntime } from '../../../../../services/runnerServerRuntime';\n\nexport const RUNNER_SERVER_SCOPE_SERVICE = 'doom/runner-server-scope';\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n 'doom/runner-server-scope': RunnerServerScope;\n }\n}\n\nexport const createRunnerServerRoot = ({ host, agent, signal }: DoomServerPluginContext) => {\n if (!host.context.directEvents) throw new Error('Runner headless facet requires the session direct event bus.');\n if (!host.context.cwd || !host.context.environment) {\n throw new Error('Runner headless facet requires session cwd and environment.');\n }\n const dependencies = createRunnerDependencies({\n cwd: host.context.cwd,\n environment: host.context.environment,\n ...(host.context.executionBudget === undefined ? {} : { executionBudget: host.context.executionBudget }),\n });\n const execution = agent?.context;\n const wakeAgent =\n execution &&\n (async (content: string, requestId: string): Promise<void> => {\n try {\n // 'steer' joins a running turn and starts one on an idle session.\n // Reuse completion identity across retries and acknowledge admission, not settlement.\n if (!execution.session.admitPrompt) throw new Error('The session cannot admit a runner notification.');\n await execution.session.admitPrompt(content, 'steer', undefined, requestId);\n } catch (error) {\n // Keep completion ownership with the watcher until admission succeeds.\n await Promise.resolve()\n .then(() => execution.client.notify({ body: String(error), level: 'warning' }))\n .catch(() => undefined);\n throw error;\n }\n });\n const runtime = createRunnerServerRuntime(dependencies, host.context.directEvents, wakeAgent);\n const bash = (wakeOnExit: boolean) =>\n createHeadlessBashTool(\n {\n run: async (request) => {\n await runtime.ensureSession(request.sessionId);\n const result = await dependencies.bashRunService.run({\n ...request,\n ...(signal ? { signal: request.signal ? AbortSignal.any([signal, request.signal]) : signal } : {}),\n });\n // Without this, nothing tells the agent the runner ended.\n if (wakeOnExit && result.kind === 'promoted') runtime.watchRunner(result.id);\n return result;\n },\n },\n summarizeLog,\n );\n const value = {\n tool: bash(true),\n // Remote MCP runners must not wake the local agent; the remote caller polls instead.\n remoteTool: bash(false),\n command: createHeadlessRunnersCommand(dependencies),\n };\n return {\n value,\n services: [\n runtime.backgroundWorkPlugin,\n (context: Context) => {\n context.plugin((providerContext) => providerContext.provide(RUNNER_SERVER_SCOPE_SERVICE, value));\n },\n ],\n activities: [runtime.activity],\n onStop: runtime.dispose,\n };\n};\n\nexport type RunnerServerScope = Awaited<ReturnType<typeof createRunnerServerRoot>>['value'];\n","import { defineRoot } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { createRunnerServerRoot } from './_lib/serverRoot';\n\nexport default defineRoot(createRunnerServerRoot);\n","import type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { RUNNER_RUNS_TYPE } from '../../constants/webRunners';\nimport { type RunnerRunView } from '../../types/webRunners';\n\nfunction runnerPayload(value: unknown): value is { runs: RunnerRunView[] } {\n return typeof value === 'object' && value !== null && Array.isArray((value as { runs?: unknown }).runs);\n}\n\n/**\n * The runners data channel consumes lifecycle-owned snapshots from the session\n * server. Durable registry files are read by the owning session only, never by\n * this hub channel.\n */\nexport function createRunnersChannel(): DoomHubChannel {\n return {\n frameType: RUNNER_RUNS_TYPE,\n start(host) {\n const latest = new Map<string, RunnerRunView[]>();\n const subscriptions = new Map<string, () => void>();\n const source: DoomHubChannelSource = {\n payloadFor(scope) {\n const runs = latest.get(scope.sessionId);\n return runs === undefined ? undefined : { runs };\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n latest.delete(scope.sessionId);\n const unsubscribe = host.directEvents.subscribe(\n RUNNER_RUNS_TYPE,\n scope.sessionId,\n (payload) => {\n if (!runnerPayload(payload)) return;\n latest.set(scope.sessionId, payload.runs);\n host.publish(scope.sessionId, payload);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n };\n return source;\n },\n };\n}\n","import { defineChannel } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomHubChannel } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { createRunnersChannel } from '../../../services/runnersChannel';\nexport default defineChannel(function runnerRunsChannel(): DoomHubChannel {\n return createRunnersChannel();\n});\n","export const LOG_DIR_NAME = 'logs';\nexport const STATE_EXTENSION = '.json';\nexport const RUNNING_STATE = 'running';\nexport const STATE_POLL_MS = 1000;\nexport const HEARTBEAT_MS = 15_000;\nexport const DEFAULT_LINES = 200;\nexport const MAX_LINES = 5000;\nexport const SAFE_SEGMENT = /^[\\w.@-]+$/;\nexport const SCREEN_POLL_MS = 100;\nexport const RAW_LOG_SUFFIX = '.raw';\nexport const MAX_INPUT_CHARS = 4096;\nexport const NOT_ATTACHABLE = 'No attachable runner by that name in this session.';\n","export const RUNNER_API_BASE_PATH = 'runner';\nexport const RUN_ID_PARAM = 'runId';\nexport const RUNNER_SCREEN_EVENT = 'screen';\nexport const RUNNER_LOG_STREAM_EVENT = 'append';\nexport const RUNNER_LOG_PING_EVENT = 'ping';\n","import fs from 'node:fs';\n\nimport { POLL_MS } from '../../constants/logTail';\nimport type { ILogTail, LogTailHandle, LogTailOptions } from '../../types/logTail';\n\n/** How often the tail restats the file; the same cadence the CLI's --follow uses. */\n\n/**\n * Polling tail: read the delta between the last offset and the file's current\n * size, and hand back only complete lines.\n *\n * Same reliability posture as this package's runner watcher: the poll is the\n * mechanism, not an accelerator for a change notification. fs.watchFile only\n * fires when it observes a difference from a baseline it took itself, so a\n * write that lands before its first stat is a change it never reports; a\n * restat every tick has no such window.\n *\n * A write that lands mid-line is common, so the trailing fragment is held\n * until its newline arrives rather than emitted as a line that will change.\n */\nexport class LogTail implements ILogTail {\n follow(logPath: string, options: LogTailOptions): LogTailHandle {\n let offset = options.from;\n let pending = '';\n let closed = false;\n\n const drain = (size: number): void => {\n // A shrunken file was rotated or truncated; start over rather than\n // reading from an offset that now points into different content.\n if (size < offset) {\n offset = 0;\n pending = '';\n }\n if (size === offset) return;\n const length = size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = fs.openSync(logPath, 'r');\n let bytesRead: number;\n try {\n bytesRead = fs.readSync(descriptor, buffer, 0, length, offset);\n } finally {\n fs.closeSync(descriptor);\n }\n offset = size;\n if (bytesRead <= 0) return;\n pending += buffer.subarray(0, bytesRead).toString('utf8');\n const lines = pending.split('\\n');\n pending = lines.pop() ?? '';\n if (lines.length > 0) options.onLines(lines, offset - Buffer.byteLength(pending, 'utf8'));\n };\n\n const tick = (): void => {\n if (closed) return;\n try {\n drain(fs.statSync(logPath).size);\n } catch (error) {\n // The runner has not opened its log yet; the next tick settles it.\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;\n options.onError(error instanceof Error ? error : new Error(String(error)));\n }\n };\n\n const timer = setInterval(tick, POLL_MS);\n // Following a log must never be the reason a process stays alive.\n timer.unref?.();\n return {\n close() {\n if (closed) return;\n closed = true;\n clearInterval(timer);\n },\n };\n }\n}\n","import type { LogSlice } from './logReader';\n\n/**\n * The runner log API, shared by this package's session-scoped routes and its\n * cockpit plugin. The two halves run in different processes, so the wire\n * vocabulary is declared here: the browser half may reach `src/types` and\n * nothing else on the server side.\n *\n * Where the routes live is not here. That is `src/types/apiRoutes`, which both\n * halves read, and the generated client turns into the URLs the page addresses.\n * The file a run writes to is still read from its metadata record by the server\n * alone.\n */\n\n/**\n * One chunk of an attached pane's output.\n *\n * Base64 because these are terminal bytes, escape sequences and all, and a\n * server-sent event is a line-oriented text frame that would mangle them.\n */\nexport interface RunnerScreenEvent {\n chunk: string;\n /** Byte offset just past this chunk, so a reconnect resumes rather than replays. */\n offset?: number;\n /** Set once the runner exits, so the page can stop watching. */\n ended?: boolean;\n}\n\n/** The body of a keystroke POST. Text, not key codes: the pane wants bytes. */\nexport interface RunnerInputRequest {\n text: string;\n}\n\n/** The named SSE event carrying a RunnerScreenEvent payload. */\n\n/**\n * A log request as query parameters. `grep` is a literal substring, not a\n * regular expression, because that is what the reader matches; `lines` bounds\n * the answer to the last N lines of whatever survived the filter.\n */\nexport interface RunnerLogQueryParams {\n lines?: number;\n grep?: string;\n ignoreCase?: boolean;\n contextLines?: number;\n}\n\n/** The slice the log route answers with, plus which runner it came from. */\nexport interface RunnerLogResponse extends LogSlice {\n runId: string;\n /** True while the runner is still writing, so the page knows following is worth offering. */\n running: boolean;\n}\n\n/**\n * One server-sent event on the follow stream: the lines appended since the last\n * one. The stream never filters: a page that sets a query stops following and\n * reads a filtered slice instead, because grep with context is a whole-file\n * answer and cannot be assembled from a chunk at a time.\n */\nexport interface RunnerLogStreamEvent {\n /** Appended lines, in order; empty when the event only reports the runner ending. */\n lines: string[];\n /**\n * Byte offset just past the last line in this event.\n *\n * A follower that loses the stream resumes here. Without it the page could\n * only fall back to the offset its last slice reported, which every line\n * delivered since has already moved past, so a reconnect would replay them.\n */\n offset?: number;\n /** Set once the runner exits, so the page can stop following without polling the run list. */\n ended?: boolean;\n}\n\n/** The named SSE event carrying a RunnerLogStreamEvent payload. */\n\n/**\n * The named SSE event that carries nothing.\n *\n * A runner can be alive and silent for minutes, which on the wire is\n * indistinguishable from a connection that died. This is sent on a timer so\n * the socket keeps proving itself. Readers that only listen for `append`\n * ignore it, which is what makes it safe to add.\n */\n\n/** Query parameter names, shared so the page and the route cannot drift apart. */\nexport const RUNNER_LOG_PARAMS = {\n lines: 'lines',\n grep: 'grep',\n ignoreCase: 'ignoreCase',\n contextLines: 'contextLines',\n from: 'from',\n} as const;\n","import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport { RUN_ID_PARAM } from '../constants/webRunnerLog';\nimport { RUNNER_LOG_PARAMS, type RunnerLogResponse } from './webRunnerLog';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path: the build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/runner/`, so the mount is\n * stated once, by the folder that creates it. This package stated it four times\n * before, once per URL builder, each of them re-spelling `/plugins/` and the\n * session prefix by hand.\n *\n * The Hono app registers these paths verbatim and the browser addresses them,\n * so a path cannot move on one side alone. `:runId` is Hono's own parameter\n * syntax for that reason; a caller supplies the value as `params`, and the\n * client encodes the segment.\n */\nexport default defineApiRoutes({\n log: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/log`,\n query: [\n RUNNER_LOG_PARAMS.lines,\n RUNNER_LOG_PARAMS.grep,\n RUNNER_LOG_PARAMS.ignoreCase,\n RUNNER_LOG_PARAMS.contextLines,\n ],\n response: apiResponse<RunnerLogResponse>(),\n },\n /**\n * The live tail of that same log.\n *\n * `stream: true`, so the client exposes a URL and no call method: the\n * consumer is the browser's own `EventSource`, and a relayed call would\n * buffer a stream that never ends.\n */\n logStream: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/log/stream`,\n query: [RUNNER_LOG_PARAMS.from],\n stream: true,\n },\n /** The unscrubbed pane bytes, for a terminal emulator rather than a reader. */\n screenStream: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/screen/stream`,\n query: [RUNNER_LOG_PARAMS.from],\n stream: true,\n },\n input: {\n method: 'POST',\n path: `/runners/:${RUN_ID_PARAM}/screen/input`,\n },\n});\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/packageApi';\nimport { Hono } from 'hono';\nimport { streamSSE } from 'hono/streaming';\n\nimport {\n DEFAULT_LINES,\n HEARTBEAT_MS,\n LOG_DIR_NAME,\n MAX_INPUT_CHARS,\n MAX_LINES,\n NOT_ATTACHABLE,\n RAW_LOG_SUFFIX,\n RUNNING_STATE,\n SAFE_SEGMENT,\n SCREEN_POLL_MS,\n STATE_EXTENSION,\n STATE_POLL_MS,\n} from '../../constants/runnerLogApi';\nimport {\n RUN_ID_PARAM,\n RUNNER_API_BASE_PATH,\n RUNNER_LOG_PING_EVENT,\n RUNNER_LOG_STREAM_EVENT,\n RUNNER_SCREEN_EVENT,\n} from '../../constants/webRunnerLog';\nimport { LogReader } from '../../services/logReader';\nimport { LogTail } from '../../services/logTail';\nimport { PtyBackendChain } from '../../services/ptyBackendChain';\nimport { RmuxBackend } from '../../services/rmuxBackend';\nimport { RunnerPaths, resolveRunnerStoreDirectory, runnerStateDirFor } from '../../services/runnerPaths';\nimport { TmuxBackend } from '../../services/tmuxBackend';\nimport { parseRunnerRecord } from '../../services/webRunnerRuns';\nimport routes from '../../types/apiRoutes';\nimport type { ILogReader, LogQuery } from '../../types/logReader';\nimport type { ILogTail } from '../../types/logTail';\nimport type { IRmuxBackend } from '../../types/rmuxBackend';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport {\n RUNNER_LOG_PARAMS,\n type RunnerLogResponse,\n type RunnerLogStreamEvent,\n type RunnerScreenEvent,\n} from '../../types/webRunnerLog';\n\n/** How often a follow re-reads the record to notice the runner exiting. */\n/**\n * How often a silent but living stream proves it is still there. Short enough\n * that an idle proxy does not reap the socket, long enough to cost nothing.\n */\n/** Kept in step with LogQuery's own default, so an unasked-for tail is the same size everywhere. */\n/** A page that asks for more than this gets this; the whole point of the route is a bounded read. */\n/** Path segments the client supplies are names, never paths. */\n/** Backends that expose a pane another process can read and write. */\nconst MULTIPLEXER_BACKENDS: ReadonlySet<string> = new Set(['rmux', 'tmux']);\n/** How often an attached pane's new bytes are picked up. */\n/** The unscrubbed copy the runner's sink writes beside the log, for interactive runs. */\n/** One keystroke batch; a paste is not a file transfer. */\n/** Said the same way for every reason, so the route never reports which check failed. */\n\nexport interface RunnerLogApiOptions {\n /** The session these routes answer for; the host owns exactly one. */\n sessionId: string;\n /** The store root; defaults to this environment's agent directory. */\n storeDir?: string;\n environment?: Readonly<Record<string, string | undefined>>;\n cwd?: string;\n logReader?: ILogReader;\n logTail?: ILogTail;\n /**\n * Reaches a live pane to read its screen and send it input.\n *\n * A session API is handed nothing but its session id, so it cannot touch the\n * in-process PTY handles the extension holds. This talks to the multiplexer\n * over its own socket instead, the same way the CLI's `input` verb does,\n * which is what makes an attached pane reachable across processes at all.\n */\n pane?: IRmuxBackend;\n}\n\n/** Rejects anything that could climb out of the session's own directory before a path is built. */\nfunction isSafeSegment(value: string): boolean {\n return value !== '' && value !== '.' && value !== '..' && SAFE_SEGMENT.test(value);\n}\n\n/**\n * The log file a run wrote, or undefined when there is no such run or its\n * record points somewhere it has no business pointing.\n *\n * The record names its own log path, and that record is written by another\n * process, so the path is checked rather than trusted: it must sit inside this\n * session's own logs directory. Without that check a doctored record would\n * turn this route into an arbitrary file read.\n */\nfunction resolveLogFile(stateDir: string, logsDir: string, sessionId: string, runId: string): RunnerRecord | undefined {\n const statePath = path.join(stateDir, `${runId}${STATE_EXTENSION}`);\n let raw: string;\n try {\n raw = fs.readFileSync(statePath, 'utf8');\n } catch {\n return undefined; // No such run in this session; the route answers 404.\n }\n const record = parseRunnerRecord(raw);\n if (record === undefined || record.id !== runId || record.sessionId !== sessionId) return undefined;\n const resolved = path.resolve(record.logPath);\n if (resolved !== logsDir && !resolved.startsWith(`${logsDir}${path.sep}`)) return undefined;\n return record;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction numberParam(value: string | undefined, fallback: number, max: number): number {\n const parsed = value === undefined ? Number.NaN : Number.parseInt(value, 10);\n if (!Number.isFinite(parsed) || parsed < 0) return fallback;\n return Math.min(parsed, max);\n}\n\nfunction logQueryOf(url: URL): LogQuery {\n const grep = url.searchParams.get(RUNNER_LOG_PARAMS.grep) ?? undefined;\n return {\n lines: numberParam(url.searchParams.get(RUNNER_LOG_PARAMS.lines) ?? undefined, DEFAULT_LINES, MAX_LINES),\n ...(grep === undefined || grep === '' ? {} : { grep }),\n ignoreCase: url.searchParams.get(RUNNER_LOG_PARAMS.ignoreCase) === 'true',\n contextLines: numberParam(url.searchParams.get(RUNNER_LOG_PARAMS.contextLines) ?? undefined, 0, MAX_LINES),\n };\n}\n\n/**\n * This package's HTTP surface: one runner's log, as a bounded slice and as a\n * live tail.\n *\n * The routes are mounted inside one session's own server, so they name a runner\n * and nothing else; the session is the host, not a parameter. The reader only\n * ever returns the last N lines of what matched, so both routes stay bounded no\n * matter how large the file on disk is.\n *\n * Paths come from src/types/apiRoutes, the one table the browser's generated\n * client reads too, so this app and the cockpit cannot disagree about where a\n * route lives. They stay relative to the mount the host strips.\n */\nexport function createRunnerLogApi(options: RunnerLogApiOptions): Hono {\n const logReader = options.logReader ?? new LogReader();\n const logTail = options.logTail ?? new LogTail();\n const sessionId = options.sessionId;\n const paths = new RunnerPaths(options.cwd, options.environment);\n const storeDir = options.storeDir ?? resolveRunnerStoreDirectory(options.environment ?? process.env, options.cwd);\n const stateDir =\n options.storeDir === undefined ? paths.stateDirectory(sessionId) : runnerStateDirFor(storeDir, sessionId);\n const logsDir =\n options.storeDir === undefined ? paths.logDirectory(sessionId) : path.join(storeDir, sessionId, LOG_DIR_NAME);\n const app = new Hono();\n\n // Built on first use, not on mount: most sessions never attach to a pane,\n // and constructing this probes the filesystem for a multiplexer binary.\n let paneBackend: IRmuxBackend | undefined = options.pane;\n const pane = (): IRmuxBackend => {\n paneBackend ??= (() => {\n const paths = new RunnerPaths(options.cwd, options.environment);\n return new PtyBackendChain(new RmuxBackend(paths), new TmuxBackend(paths));\n })();\n return paneBackend;\n };\n\n /** The record behind a request, or undefined when this session has no such run. */\n const runnerOf = (runId: string): RunnerRecord | undefined =>\n isSafeSegment(runId) ? resolveLogFile(stateDir, logsDir, sessionId, runId) : undefined;\n\n /**\n * The pane a request may attach to.\n *\n * Same posture as the log routes: the record is written by another process,\n * so nothing on it is trusted until it is checked. The target must be one\n * this package owns, the run must still be up, and it must have been started\n * interactive. A non-interactive run has no terminal to type into, and a\n * subprocess-backed run has no pane at all.\n */\n const attachableOf = (runId: string): { record: RunnerRecord; target: string } | undefined => {\n const record = runnerOf(runId);\n if (record === undefined || record.state !== RUNNING_STATE || record.interactive !== true) return undefined;\n const target = record.backendTarget;\n if (target === undefined || !MULTIPLEXER_BACKENDS.has(record.backend) || !isSafeSegment(target)) return undefined;\n return { record, target };\n };\n app.get(routes.log.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const record = runnerOf(runId);\n if (record === undefined) return context.json({ error: `No runner '${runId}' in this session.` }, 404);\n const slice = logReader.read(record.logPath, logQueryOf(new URL(context.req.url)));\n const body: RunnerLogResponse = { ...slice, runId, running: record.state === RUNNING_STATE };\n return context.json(body);\n });\n\n app.get(routes.logStream.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const record = runnerOf(runId);\n if (record === undefined) return context.json({ error: `No runner '${runId}' in this session.` }, 404);\n const from = numberParam(\n new URL(context.req.url).searchParams.get(RUNNER_LOG_PARAMS.from) ?? undefined,\n 0,\n Number.MAX_SAFE_INTEGER,\n );\n\n return streamSSE(context, async (stream) => {\n const send = (event: RunnerLogStreamEvent): Promise<void> =>\n stream.writeSSE({ event: RUNNER_LOG_STREAM_EVENT, data: JSON.stringify(event) });\n\n // The tail and the state poll both push onto one queue, so the stream\n // writes in the order things happened rather than interleaving two\n // writers onto the same socket.\n let pump: Promise<void> = Promise.resolve();\n const queue = (event: RunnerLogStreamEvent): void => {\n pump = pump.then(() => send(event)).catch(() => undefined);\n };\n\n let finish = (): void => undefined;\n const done = new Promise<void>((resolve) => {\n finish = resolve;\n });\n const handle = logTail.follow(record.logPath, {\n from,\n onLines: (lines, completeThrough) => queue({ lines, offset: completeThrough }),\n onError: () => finish(),\n });\n // A finished runner writes nothing more, so the page is told once and\n // the stream ends rather than holding a socket open forever.\n const poll = setInterval(() => {\n const current = runnerOf(runId);\n if (current !== undefined && current.state === RUNNING_STATE) return;\n queue({ lines: [], ended: true });\n finish();\n }, STATE_POLL_MS);\n // A runner can be alive and produce nothing for minutes. Without this the\n // page cannot tell that from a stream that quietly died.\n const heartbeat = setInterval(() => {\n pump = pump.then(() => stream.writeSSE({ event: RUNNER_LOG_PING_EVENT, data: '' })).catch(() => undefined);\n }, HEARTBEAT_MS);\n\n stream.onAbort(() => finish());\n await done;\n clearInterval(poll);\n clearInterval(heartbeat);\n handle.close();\n await pump;\n });\n });\n\n app.get(routes.screenStream.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const attachable = attachableOf(runId);\n if (attachable === undefined) return context.json({ error: NOT_ATTACHABLE }, 404);\n // The record's log path is already proven to sit inside this session's own\n // logs directory, and the raw copy is that same path with a suffix, so it\n // cannot point anywhere new.\n const rawPath = `${attachable.record.logPath}${RAW_LOG_SUFFIX}`;\n const from = numberParam(\n new URL(context.req.url).searchParams.get(RUNNER_LOG_PARAMS.from) ?? undefined,\n 0,\n Number.MAX_SAFE_INTEGER,\n );\n\n return streamSSE(context, async (stream) => {\n let pump: Promise<void> = Promise.resolve();\n const queue = (event: RunnerScreenEvent): void => {\n pump = pump\n .then(() => stream.writeSSE({ event: RUNNER_SCREEN_EVENT, data: JSON.stringify(event) }))\n .catch(() => undefined);\n };\n\n let finish = (): void => undefined;\n const done = new Promise<void>((resolve) => {\n finish = resolve;\n });\n\n let offset = from;\n const drain = (): void => {\n let size: number;\n try {\n size = fs.statSync(rawPath).size;\n } catch {\n return; // The sink has not opened the file yet; the next tick settles it.\n }\n // The sink starts the file over when it passes its ceiling, so a\n // smaller file is a fresh one rather than a bad offset.\n if (size < offset) offset = 0;\n if (size === offset) return;\n const length = size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = fs.openSync(rawPath, 'r');\n let read: number;\n try {\n read = fs.readSync(descriptor, buffer, 0, length, offset);\n } finally {\n fs.closeSync(descriptor);\n }\n if (read <= 0) return;\n offset += read;\n // Bytes, not lines: a terminal's escape sequences straddle newlines and\n // mean nothing once split on them.\n queue({ chunk: buffer.subarray(0, read).toString('base64'), offset });\n };\n\n const poll = setInterval(() => {\n const current = runnerOf(runId);\n drain();\n if (current === undefined || current.state !== RUNNING_STATE) {\n queue({ chunk: '', offset, ended: true });\n finish();\n }\n }, SCREEN_POLL_MS);\n\n stream.onAbort(() => finish());\n await done;\n clearInterval(poll);\n await pump;\n });\n });\n\n /**\n * Types into a live pane.\n *\n * This is the one route in the package that writes rather than reads, so it\n * is deliberately narrow: an owned target, still running, started\n * interactive, and a body that is nothing but text.\n */\n app.post(routes.input.path, async (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const attachable = attachableOf(runId);\n if (attachable === undefined) return context.json({ error: NOT_ATTACHABLE }, 404);\n\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'Expected a JSON body carrying text.' }, 400);\n }\n const text = isRecord(body) ? body.text : undefined;\n if (typeof text !== 'string' || text === '') return context.json({ error: 'Expected a non-empty text.' }, 400);\n if (text.length > MAX_INPUT_CHARS) return context.json({ error: 'That is more input than a pane takes.' }, 413);\n\n const delivered = await pane().input(attachable.target, text);\n if (!delivered) return context.json({ error: 'The runner did not take the input.' }, 502);\n return context.json({ delivered: true });\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built session entry. */\nexport const api: DoomApi = {\n basePath: RUNNER_API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n // The host is one session's server, so its id is the whole scope; a hub\n // would have handed no session at all, and there is nothing to answer.\n const app = createRunnerLogApi({\n sessionId: context.sessionId ?? '',\n cwd: context.cwd,\n environment: context.environment,\n });\n return {\n fetch: (request) => app.fetch(request),\n // Nothing outlives a request: a follow's watch and its poll are both\n // torn down when its own stream ends or the socket aborts.\n close: () => undefined,\n };\n },\n};\n","import { defineRoute } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { api } from '../../../../../../services/runnerLogApi';\n\n/**\n * Mounts this package's HTTP API at `runner`, which the folder names.\n *\n * The folder is the declaration: the build reads the base path off it and hands\n * the browser a client bound to the same segment, so the mount is stated once\n * rather than in a constant and in four URL builders that were each free to\n * drift.\n */\nexport default defineRoute(api);\n","import { defineServerCommand, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { RunnerServerScope } from '../_lib/serverRoot';\nexport default defineServerCommand(\n (context: WithRoot<DoomServerPluginContext, RunnerServerScope>) => context.root.command,\n);\n","import { defineServerTool, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { RunnerServerScope } from '../_lib/serverRoot';\nexport default defineServerTool((context: WithRoot<DoomServerPluginContext, RunnerServerScope>) => context.root.tool);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\nimport { composeRootHooks } from '@agimon-ai/doompi-core/extensionFile';\n\nimport rootSession from '../src/extensions/workspaces/sessions/(backend)/root.server';\nimport channelRunnerRuns from '../src/extensions/(backend)/channel/runnerRuns.server';\nimport apiRoute from '../src/extensions/workspaces/sessions/(backend)/api/runner/route.server';\nimport commandRunners from '../src/extensions/workspaces/sessions/(backend)/command/runners.server';\nimport toolBash from '../src/extensions/workspaces/sessions/(backend)/tool/bash.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/doompi-runner',\n global: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n }),\n workspace: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n }),\n session: async (context) => {\n const scopeSession = await rootSession(context);\n const contextSession = { ...context, root: scopeSession.value };\n return {\n ...composeRootHooks(scopeSession),\n get api(): DoomServerSessionPlugin['api'] { return [at(apiRoute, contextSession)]; },\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n get commands(): DoomServerSessionPlugin['commands'] { return [via({ name: 'runners' }, at(commandRunners, contextSession))]; },\n get tools(): DoomServerSessionPlugin['tools'] { return [via({ name: 'bash' }, at(toolBash, contextSession))]; },\n services: [...(scopeSession.services ?? [])],\n get activities(): DoomServerSessionPlugin['activities'] { return [...(scopeSession.activities ?? [])]; },\n };\n },\n});\n\nexport default facet;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.mjs","names":["routes","channelRunnerRuns","rootSession","apiRoute","commandRunners","toolBash"],"sources":["../../src/constants/webRunners.ts","../../src/constants/webRunnerRuns.ts","../../src/services/webRunnerRuns/index.ts","../../src/services/runnerServerRuntime/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/root.server.ts","../../src/services/runnersChannel/index.ts","../../src/extensions/(backend)/channel/runnerRuns.server.ts","../../src/constants/runnerLogApi.ts","../../src/constants/webRunnerLog.ts","../../src/services/logTail/index.ts","../../src/types/webRunnerLog.ts","../../src/types/apiRoutes.ts","../../src/services/runnerLogApi/index.ts","../../src/extensions/workspaces/sessions/(backend)/api/runner/route.server.ts","../../src/extensions/workspaces/sessions/(backend)/command/runners.server.ts","../../src/extensions/workspaces/sessions/(backend)/tool/bash.server.ts","../../generated/server.ts"],"sourcesContent":["export const RUNNER_RUNS_TYPE = 'runner_runs';\n","export const FINISHED_RUNNER_RETENTION_MS = 600_000;\n","import { FINISHED_RUNNER_RETENTION_MS } from '../../constants/webRunnerRuns';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport type { RunnerRunView } from '../../types/webRunners';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** How long a finished runner stays in the cockpit's feed after it exited. */\n\n/** One on-disk metadata record, or undefined when the text is not a runner record. */\nexport function parseRunnerRecord(raw: string): RunnerRecord | undefined {\n try {\n const parsed: unknown = JSON.parse(raw);\n return isRunnerRecord(parsed) ? parsed : undefined;\n } catch {\n return undefined; // Mid-write or foreign file; the next scan settles it.\n }\n}\n\nexport function toRunnerRunView(record: RunnerRecord): RunnerRunView {\n return {\n id: record.id,\n name: record.name,\n pid: record.pid,\n command: record.command,\n cwd: record.cwd,\n interactive: record.interactive,\n backend: record.backend,\n state: record.state,\n promoted: record.promoted,\n startedAt: record.startedAt,\n logPath: record.logPath,\n ...(record.exit\n ? {\n exit: {\n reason: record.exit.reason,\n code: record.exit.code,\n signal: record.exit.signal,\n ...(record.exit.terminationReason === undefined\n ? {}\n : { terminationReason: record.exit.terminationReason }),\n ...(record.exit.stopReason === undefined ? {} : { stopReason: record.exit.stopReason }),\n finishedAt: record.exit.finishedAt,\n },\n }\n : {}),\n };\n}\n\nfunction finishedAt(record: RunnerRecord): number {\n const parsed = record.exit === undefined ? Number.NaN : Date.parse(record.exit.finishedAt);\n return Number.isFinite(parsed) ? parsed : Date.parse(record.startedAt);\n}\n\n/**\n * The feed the cockpit shows: running runners first, newest start first, then\n * the recently finished ones, newest exit first. Finished runners older than\n * the retention window are left out; the registry keeps them for the CLI.\n */\nexport function presentRunnerRuns(records: readonly RunnerRecord[], now: number): RunnerRunView[] {\n const running = records\n .filter((record) => record.state === 'running')\n .sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n const finished = records\n .filter((record) => record.state === 'completed' && now - finishedAt(record) <= FINISHED_RUNNER_RETENTION_MS)\n .sort((left, right) => finishedAt(right) - finishedAt(left));\n return [...running, ...finished].map(toRunnerRunView);\n}\n","import {\n DOOM_BACKGROUND_WORK_SERVICE,\n readDoomBackgroundWorkService,\n type BackgroundProviderWorkItem,\n type BackgroundWorkProviderHandle,\n} from '@agimon-ai/doompi-core/backgroundWork';\nimport type { DoomHeadlessActivity, DoomHeadlessExecutionContext } from '@agimon-ai/doompi-core/headless';\nimport type { DoomServerHostService } from '@agimon-ai/doompi-core/serverFacet';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { BACKGROUND_WORK_PROVIDER } from '../../constants/runnerRuntime';\nimport { RUNNER_RUNS_TYPE } from '../../constants/webRunners';\nimport { reconcileActiveRunners, stopRunnerProcess } from '../reconcile';\nimport type { RunnerDependencies } from '../runnerDependencies/type';\nimport { formatRunnerFinished } from '../runnerRecord';\nimport { presentRunnerRuns } from '../webRunnerRuns';\n\nconst COMPLETION_RETRY_MS = 1_000;\n\nexport interface RunnerServerRuntime {\n readonly container: RunnerDependencies;\n readonly activity: DoomHeadlessActivity;\n readonly backgroundWorkPlugin: (context: Context) => void;\n ensureSession(sessionId: string): Promise<void>;\n /** Wakes the agent once when this runner, which it sent to the background, exits. */\n watchRunner(id: string): void;\n dispose(this: void): Promise<void>;\n}\nexport function createRunnerServerRuntime(\n container: RunnerDependencies,\n directEvents: NonNullable<DoomServerHostService['context']['directEvents']>,\n /** Absent when no agent runs in this session, so there is nobody to wake. */\n wakeAgent?: (content: string, requestId: string) => Promise<void>,\n): RunnerServerRuntime {\n let sessionId: string | undefined;\n let supervision: Promise<void> | undefined;\n let runtimeDisposal: Promise<void> | undefined;\n let backgroundWorkItems: BackgroundProviderWorkItem[] = [];\n let backgroundWorkProvider: BackgroundWorkProviderHandle | undefined;\n // Nothing else tells the model a background runner ended, so without this an\n // agent that finished its turn waiting on one idles until a person prompts it.\n const watched = new Set<string>();\n let unsubscribeWatched: (() => void) | undefined;\n let wakes: Promise<void> = Promise.resolve();\n let closing = false;\n let retryTimer: ReturnType<typeof setTimeout> | undefined;\n\n // The activity is optional and source-gated. The retained facet owns the\n // session runtime, so disabling the activity must not tear down its jobs.\n const ensureSession = (requestedSessionId: string): Promise<void> => {\n if (closing) return Promise.reject(new Error('Runner runtime is closing.'));\n if (sessionId !== undefined && sessionId !== requestedSessionId) {\n throw new Error(`Runner runtime belongs to session ${sessionId}.`);\n }\n sessionId ??= requestedSessionId;\n supervision ??= (async () => {\n container.paths.setSessionId(requestedSessionId);\n if ((await container.lifeline.arm(requestedSessionId)) === undefined) {\n throw new Error('Could not arm runner supervision lifeline.');\n }\n const reconciled = await reconcileActiveRunners({\n registry: container.runnerRegistry,\n launcher: container.launcher,\n rmuxBackend: container.rmuxBackend,\n processControl: container.processControl,\n currentHostPid: process.pid,\n startup: false,\n active: await container.runnerRegistry.listBySession(requestedSessionId),\n });\n for (const error of reconciled.errors) process.emitWarning(error);\n if (!closing) {\n unsubscribeRunnerUpdates ??= container.runnerRegistry.subscribe(\n () => requestPublish(requestedSessionId),\n requestedSessionId,\n );\n requestPublish(requestedSessionId);\n }\n })().catch((error: unknown) => {\n supervision = undefined;\n throw error;\n });\n return supervision;\n };\n\n const disposeRuntime = (): Promise<void> => {\n runtimeDisposal ??= (async () => {\n await supervision?.catch((error: unknown) =>\n process.emitWarning(`Could not initialize headless runner supervision: ${String(error)}`),\n );\n const ownedSessionId = sessionId;\n if (ownedSessionId === undefined) return;\n const records = await container.runnerRegistry.listBySession(ownedSessionId).catch(() => []);\n await Promise.all(\n records.map(async (record) => {\n try {\n const stopped = await stopRunnerProcess(record, container.launcher, container.rmuxBackend, {\n registry: container.runnerRegistry,\n intent: {\n reason: 'stopped',\n terminationReason: 'parent_session_cleanup',\n stopReason: 'session ended',\n },\n });\n if (!stopped && container.processControl.isAlive(record.pid)) {\n process.emitWarning(`Could not stop runner ${record.id} during session shutdown`);\n return;\n }\n await container.runnerRegistry.complete(\n record.id,\n { reason: 'stopped', code: null, signal: null, stopReason: 'session ended' },\n ownedSessionId,\n );\n } catch (error) {\n process.emitWarning(`Could not clean up runner ${record.id}: ${String(error)}`);\n }\n }),\n );\n try {\n await container.ptyHost.disposeAll();\n } finally {\n container.lifeline.dispose();\n container.runnerRegistry.close();\n }\n })();\n return runtimeDisposal;\n };\n\n const publishRuns = async (ownedSessionId: string): Promise<void> => {\n try {\n const records = await container.runnerRegistry.listAll(ownedSessionId);\n const nextBackgroundWork = records\n .filter((record) => record.state === 'running' || watched.has(record.id))\n .map((record) => ({\n id: record.id,\n sessionId: record.sessionId,\n label: record.name,\n // An absent status holds ownership until completion admission succeeds.\n ...(record.state === 'running' ? { status: record.state } : {}),\n }));\n if (JSON.stringify(nextBackgroundWork) !== JSON.stringify(backgroundWorkItems)) {\n backgroundWorkItems = nextBackgroundWork;\n backgroundWorkProvider?.update();\n }\n directEvents.publish(RUNNER_RUNS_TYPE, ownedSessionId, {\n runs: presentRunnerRuns(records, Date.now()),\n });\n } catch (error) {\n process.emitWarning(`Could not publish runner state: ${String(error)}`);\n }\n };\n let publishInFlight: Promise<void> | undefined;\n let publishAgain = false;\n let unsubscribeRunnerUpdates: (() => void) | undefined;\n const requestPublish = (ownedSessionId: string): void => {\n if (closing) return;\n publishAgain = true;\n if (publishInFlight !== undefined) return;\n publishInFlight = (async () => {\n do {\n publishAgain = false;\n await publishRuns(ownedSessionId);\n } while (publishAgain && !closing);\n })()\n .catch((error: unknown) => {\n process.emitWarning(`Could not schedule runner state publication: ${String(error)}`);\n })\n .finally(() => {\n publishInFlight = undefined;\n if (publishAgain) requestPublish(ownedSessionId);\n });\n };\n\n // Sequenced so two runners exiting in the same tick each wake the agent once.\n const wakeForFinished = (ownedSessionId: string): void => {\n if (closing) return;\n wakes = wakes\n .then(async () => {\n if (closing) return;\n let retry = false;\n // Deleting the entry being visited is safe for a Set iterator.\n for (const id of watched) {\n if (closing) return;\n try {\n const record = await container.runnerRegistry.get(id, ownedSessionId);\n if (closing) return;\n if (record?.state === 'running') continue;\n // A record swept before it was seen leaves nothing to report.\n if (record)\n await wakeAgent?.(\n formatRunnerFinished(record),\n `runner-finished:${JSON.stringify([ownedSessionId, id])}`,\n );\n watched.delete(id);\n requestPublish(ownedSessionId);\n } catch (error) {\n retry = true;\n process.emitWarning(`Could not report finished runner ${id}: ${String(error)}`);\n }\n }\n if (retry && !closing && retryTimer === undefined) {\n retryTimer = setTimeout(() => {\n retryTimer = undefined;\n wakeForFinished(ownedSessionId);\n }, COMPLETION_RETRY_MS);\n retryTimer.unref?.();\n }\n if (watched.size === 0) {\n clearTimeout(retryTimer);\n retryTimer = undefined;\n unsubscribeWatched?.();\n unsubscribeWatched = undefined;\n }\n })\n .catch((error: unknown) => {\n process.emitWarning(`Could not report a finished runner: ${String(error)}`);\n });\n };\n\n const watchRunner = (id: string): void => {\n const ownedSessionId = sessionId;\n if (!wakeAgent || closing || ownedSessionId === undefined) return;\n watched.add(id);\n requestPublish(ownedSessionId);\n // Independent of the optional activity: the wake-up must not depend on a\n // browser watching the runner list.\n unsubscribeWatched ??= container.runnerRegistry.subscribe(() => wakeForFinished(ownedSessionId), ownedSessionId);\n // It may already have exited between promotion and subscription.\n wakeForFinished(ownedSessionId);\n };\n\n const activity: DoomHeadlessActivity = {\n name: 'runner',\n start: async (executionContext: DoomHeadlessExecutionContext) => {\n await ensureSession(executionContext.sessionId);\n requestPublish(executionContext.sessionId);\n return () => {\n // Let the next optional activation reconcile again, while keeping\n // the retained runtime and all child job supervisors alive.\n supervision = undefined;\n };\n },\n };\n return {\n container,\n activity,\n ensureSession,\n watchRunner,\n backgroundWorkPlugin: (context) => {\n context.inject([DOOM_BACKGROUND_WORK_SERVICE], (serviceContext) => {\n const service = readDoomBackgroundWorkService(serviceContext);\n if (!service) return undefined;\n const registration = service.register({\n provider: BACKGROUND_WORK_PROVIDER,\n listActiveWork: () => backgroundWorkItems,\n });\n backgroundWorkProvider = registration;\n return () => {\n registration.dispose();\n if (backgroundWorkProvider === registration) backgroundWorkProvider = undefined;\n };\n });\n },\n async dispose() {\n // Session cleanup completes every runner it stops, and none of those may\n // start a turn in a session that is going away.\n closing = true;\n clearTimeout(retryTimer);\n retryTimer = undefined;\n watched.clear();\n unsubscribeWatched?.();\n unsubscribeWatched = undefined;\n unsubscribeRunnerUpdates?.();\n unsubscribeRunnerUpdates = undefined;\n await wakes;\n await publishInFlight;\n await disposeRuntime();\n },\n };\n}\n","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { createHeadlessBashTool, createHeadlessRunnersCommand } from '../../../../../services/headless';\nimport { summarizeLog } from '../../../../../services/logReader';\nimport { createRunnerDependencies } from '../../../../../services/runnerDependencies';\nimport { createRunnerServerRuntime } from '../../../../../services/runnerServerRuntime';\n\nexport const RUNNER_SERVER_SCOPE_SERVICE = 'doom/runner-server-scope';\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n 'doom/runner-server-scope': RunnerServerScope;\n }\n}\n\nexport const createRunnerServerRoot = ({ host, agent, signal }: DoomServerPluginContext) => {\n if (!host.context.directEvents) throw new Error('Runner headless facet requires the session direct event bus.');\n if (!host.context.cwd || !host.context.environment) {\n throw new Error('Runner headless facet requires session cwd and environment.');\n }\n const dependencies = createRunnerDependencies({\n cwd: host.context.cwd,\n environment: host.context.environment,\n ...(host.context.executionBudget === undefined ? {} : { executionBudget: host.context.executionBudget }),\n });\n const execution = agent?.context;\n const wakeAgent =\n execution &&\n (async (content: string, requestId: string): Promise<void> => {\n try {\n // 'steer' joins a running turn and starts one on an idle session.\n // Reuse completion identity across retries and acknowledge admission, not settlement.\n if (!execution.session.admitPrompt) throw new Error('The session cannot admit a runner notification.');\n await execution.session.admitPrompt(content, 'steer', undefined, requestId);\n } catch (error) {\n // Keep completion ownership with the watcher until admission succeeds.\n await Promise.resolve()\n .then(() => execution.client.notify({ body: String(error), level: 'warning' }))\n .catch(() => undefined);\n throw error;\n }\n });\n const runtime = createRunnerServerRuntime(dependencies, host.context.directEvents, wakeAgent);\n const bash = (wakeOnExit: boolean) =>\n createHeadlessBashTool(\n {\n run: async (request) => {\n await runtime.ensureSession(request.sessionId);\n const result = await dependencies.bashRunService.run({\n ...request,\n ...(signal ? { signal: request.signal ? AbortSignal.any([signal, request.signal]) : signal } : {}),\n });\n // Without this, nothing tells the agent the runner ended.\n if (wakeOnExit && result.kind === 'promoted') runtime.watchRunner(result.id);\n return result;\n },\n },\n summarizeLog,\n );\n const value = {\n tool: bash(true),\n // Remote MCP runners must not wake the local agent; the remote caller polls instead.\n remoteTool: bash(false),\n command: createHeadlessRunnersCommand(dependencies),\n };\n return {\n value,\n services: [\n runtime.backgroundWorkPlugin,\n (context: Context) => {\n context.plugin((providerContext) => providerContext.provide(RUNNER_SERVER_SCOPE_SERVICE, value));\n },\n ],\n activities: [runtime.activity],\n onStop: runtime.dispose,\n };\n};\n\nexport type RunnerServerScope = Awaited<ReturnType<typeof createRunnerServerRoot>>['value'];\n","import { defineRoot } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { createRunnerServerRoot } from './_lib/serverRoot';\n\nexport default defineRoot(createRunnerServerRoot);\n","import type { DoomHubChannel, DoomHubChannelSource } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { RUNNER_RUNS_TYPE } from '../../constants/webRunners';\nimport { type RunnerRunView } from '../../types/webRunners';\n\nfunction runnerPayload(value: unknown): value is { runs: RunnerRunView[] } {\n return typeof value === 'object' && value !== null && Array.isArray((value as { runs?: unknown }).runs);\n}\n\n/**\n * The runners data channel consumes lifecycle-owned snapshots from the session\n * server. Durable registry files are read by the owning session only, never by\n * this hub channel.\n */\nexport function createRunnersChannel(): DoomHubChannel {\n return {\n frameType: RUNNER_RUNS_TYPE,\n start(host) {\n const latest = new Map<string, RunnerRunView[]>();\n const subscriptions = new Map<string, () => void>();\n const source: DoomHubChannelSource = {\n payloadFor(scope) {\n const runs = latest.get(scope.sessionId);\n return runs === undefined ? undefined : { runs };\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n latest.delete(scope.sessionId);\n const unsubscribe = host.directEvents.subscribe(\n RUNNER_RUNS_TYPE,\n scope.sessionId,\n (payload) => {\n if (!runnerPayload(payload)) return;\n latest.set(scope.sessionId, payload.runs);\n host.publish(scope.sessionId, payload);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n };\n return source;\n },\n };\n}\n","import { defineChannel } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomHubChannel } from '@agimon-ai/doompi-core/hubChannel';\n\nimport { createRunnersChannel } from '../../../services/runnersChannel';\nexport default defineChannel(function runnerRunsChannel(): DoomHubChannel {\n return createRunnersChannel();\n});\n","export const LOG_DIR_NAME = 'logs';\nexport const STATE_EXTENSION = '.json';\nexport const RUNNING_STATE = 'running';\nexport const STATE_POLL_MS = 1000;\nexport const HEARTBEAT_MS = 15_000;\nexport const DEFAULT_LINES = 200;\nexport const MAX_LINES = 5000;\nexport const SAFE_SEGMENT = /^[\\w.@-]+$/;\nexport const SCREEN_POLL_MS = 100;\nexport const RAW_LOG_SUFFIX = '.raw';\nexport const MAX_INPUT_CHARS = 4096;\nexport const NOT_ATTACHABLE = 'No attachable runner by that name in this session.';\n","export const RUNNER_API_BASE_PATH = 'runner';\nexport const RUN_ID_PARAM = 'runId';\nexport const RUNNER_SCREEN_EVENT = 'screen';\nexport const RUNNER_LOG_STREAM_EVENT = 'append';\nexport const RUNNER_LOG_PING_EVENT = 'ping';\n","import fs from 'node:fs';\n\nimport { POLL_MS } from '../../constants/logTail';\nimport type { ILogTail, LogTailHandle, LogTailOptions } from '../../types/logTail';\n\n/** How often the tail restats the file; the same cadence the CLI's --follow uses. */\n\n/**\n * Polling tail: read the delta between the last offset and the file's current\n * size, and hand back only complete lines.\n *\n * Same reliability posture as this package's runner watcher: the poll is the\n * mechanism, not an accelerator for a change notification. fs.watchFile only\n * fires when it observes a difference from a baseline it took itself, so a\n * write that lands before its first stat is a change it never reports; a\n * restat every tick has no such window.\n *\n * A write that lands mid-line is common, so the trailing fragment is held\n * until its newline arrives rather than emitted as a line that will change.\n */\nexport class LogTail implements ILogTail {\n follow(logPath: string, options: LogTailOptions): LogTailHandle {\n let offset = options.from;\n let pending = '';\n let closed = false;\n\n const drain = (size: number): void => {\n // A shrunken file was rotated or truncated; start over rather than\n // reading from an offset that now points into different content.\n if (size < offset) {\n offset = 0;\n pending = '';\n }\n if (size === offset) return;\n const length = size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = fs.openSync(logPath, 'r');\n let bytesRead: number;\n try {\n bytesRead = fs.readSync(descriptor, buffer, 0, length, offset);\n } finally {\n fs.closeSync(descriptor);\n }\n offset = size;\n if (bytesRead <= 0) return;\n pending += buffer.subarray(0, bytesRead).toString('utf8');\n const lines = pending.split('\\n');\n pending = lines.pop() ?? '';\n if (lines.length > 0) options.onLines(lines, offset - Buffer.byteLength(pending, 'utf8'));\n };\n\n const tick = (): void => {\n if (closed) return;\n try {\n drain(fs.statSync(logPath).size);\n } catch (error) {\n // The runner has not opened its log yet; the next tick settles it.\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;\n options.onError(error instanceof Error ? error : new Error(String(error)));\n }\n };\n\n const timer = setInterval(tick, POLL_MS);\n // Following a log must never be the reason a process stays alive.\n timer.unref?.();\n return {\n close() {\n if (closed) return;\n closed = true;\n clearInterval(timer);\n },\n };\n }\n}\n","import type { LogSlice } from './logReader';\n\n/**\n * The runner log API, shared by this package's session-scoped routes and its\n * cockpit plugin. The two halves run in different processes, so the wire\n * vocabulary is declared here: the browser half may reach `src/types` and\n * nothing else on the server side.\n *\n * Where the routes live is not here. That is `src/types/apiRoutes`, which both\n * halves read, and the generated client turns into the URLs the page addresses.\n * The file a run writes to is still read from its metadata record by the server\n * alone.\n */\n\n/**\n * One chunk of an attached pane's output.\n *\n * Base64 because these are terminal bytes, escape sequences and all, and a\n * server-sent event is a line-oriented text frame that would mangle them.\n */\nexport interface RunnerScreenEvent {\n chunk: string;\n /** Byte offset just past this chunk, so a reconnect resumes rather than replays. */\n offset?: number;\n /** Set once the runner exits, so the page can stop watching. */\n ended?: boolean;\n}\n\n/** The body of a keystroke POST. Text, not key codes: the pane wants bytes. */\nexport interface RunnerInputRequest {\n text: string;\n}\n\n/** The named SSE event carrying a RunnerScreenEvent payload. */\n\n/**\n * A log request as query parameters. `grep` is a literal substring, not a\n * regular expression, because that is what the reader matches; `lines` bounds\n * the answer to the last N lines of whatever survived the filter.\n */\nexport interface RunnerLogQueryParams {\n lines?: number;\n grep?: string;\n ignoreCase?: boolean;\n contextLines?: number;\n}\n\n/** The slice the log route answers with, plus which runner it came from. */\nexport interface RunnerLogResponse extends LogSlice {\n runId: string;\n /** True while the runner is still writing, so the page knows following is worth offering. */\n running: boolean;\n}\n\n/**\n * One server-sent event on the follow stream: the lines appended since the last\n * one. The stream never filters: a page that sets a query stops following and\n * reads a filtered slice instead, because grep with context is a whole-file\n * answer and cannot be assembled from a chunk at a time.\n */\nexport interface RunnerLogStreamEvent {\n /** Appended lines, in order; empty when the event only reports the runner ending. */\n lines: string[];\n /**\n * Byte offset just past the last line in this event.\n *\n * A follower that loses the stream resumes here. Without it the page could\n * only fall back to the offset its last slice reported, which every line\n * delivered since has already moved past, so a reconnect would replay them.\n */\n offset?: number;\n /** Set once the runner exits, so the page can stop following without polling the run list. */\n ended?: boolean;\n}\n\n/** The named SSE event carrying a RunnerLogStreamEvent payload. */\n\n/**\n * The named SSE event that carries nothing.\n *\n * A runner can be alive and silent for minutes, which on the wire is\n * indistinguishable from a connection that died. This is sent on a timer so\n * the socket keeps proving itself. Readers that only listen for `append`\n * ignore it, which is what makes it safe to add.\n */\n\n/** Query parameter names, shared so the page and the route cannot drift apart. */\nexport const RUNNER_LOG_PARAMS = {\n lines: 'lines',\n grep: 'grep',\n ignoreCase: 'ignoreCase',\n contextLines: 'contextLines',\n from: 'from',\n} as const;\n","import { apiResponse, defineApiRoutes } from '@agimon-ai/doompi-core/web';\n\nimport { RUN_ID_PARAM } from '../constants/webRunnerLog';\nimport { RUNNER_LOG_PARAMS, type RunnerLogResponse } from './webRunnerLog';\n\n/**\n * This package's routes, as data.\n *\n * No scope and no base path: the build reads both off\n * `src/extensions/workspaces/sessions/(backend)/api/runner/`, so the mount is\n * stated once, by the folder that creates it. This package stated it four times\n * before, once per URL builder, each of them re-spelling `/plugins/` and the\n * session prefix by hand.\n *\n * The Hono app registers these paths verbatim and the browser addresses them,\n * so a path cannot move on one side alone. `:runId` is Hono's own parameter\n * syntax for that reason; a caller supplies the value as `params`, and the\n * client encodes the segment.\n */\nexport default defineApiRoutes({\n log: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/log`,\n query: [\n RUNNER_LOG_PARAMS.lines,\n RUNNER_LOG_PARAMS.grep,\n RUNNER_LOG_PARAMS.ignoreCase,\n RUNNER_LOG_PARAMS.contextLines,\n ],\n response: apiResponse<RunnerLogResponse>(),\n },\n /**\n * The live tail of that same log.\n *\n * `stream: true`, so the client exposes a URL and no call method: the\n * consumer is the browser's own `EventSource`, and a relayed call would\n * buffer a stream that never ends.\n */\n logStream: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/log/stream`,\n query: [RUNNER_LOG_PARAMS.from],\n stream: true,\n },\n /** The unscrubbed pane bytes, for a terminal emulator rather than a reader. */\n screenStream: {\n method: 'GET',\n path: `/runners/:${RUN_ID_PARAM}/screen/stream`,\n query: [RUNNER_LOG_PARAMS.from],\n stream: true,\n },\n input: {\n method: 'POST',\n path: `/runners/:${RUN_ID_PARAM}/screen/input`,\n },\n});\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/packageApi';\nimport { Hono } from 'hono';\nimport { streamSSE } from 'hono/streaming';\n\nimport {\n DEFAULT_LINES,\n HEARTBEAT_MS,\n LOG_DIR_NAME,\n MAX_INPUT_CHARS,\n MAX_LINES,\n NOT_ATTACHABLE,\n RAW_LOG_SUFFIX,\n RUNNING_STATE,\n SAFE_SEGMENT,\n SCREEN_POLL_MS,\n STATE_EXTENSION,\n STATE_POLL_MS,\n} from '../../constants/runnerLogApi';\nimport {\n RUN_ID_PARAM,\n RUNNER_API_BASE_PATH,\n RUNNER_LOG_PING_EVENT,\n RUNNER_LOG_STREAM_EVENT,\n RUNNER_SCREEN_EVENT,\n} from '../../constants/webRunnerLog';\nimport { LogReader } from '../../services/logReader';\nimport { LogTail } from '../../services/logTail';\nimport { PtyBackendChain } from '../../services/ptyBackendChain';\nimport { RmuxBackend } from '../../services/rmuxBackend';\nimport { RunnerPaths, resolveRunnerStoreDirectory, runnerStateDirFor } from '../../services/runnerPaths';\nimport { TmuxBackend } from '../../services/tmuxBackend';\nimport { parseRunnerRecord } from '../../services/webRunnerRuns';\nimport routes from '../../types/apiRoutes';\nimport type { ILogReader, LogQuery } from '../../types/logReader';\nimport type { ILogTail } from '../../types/logTail';\nimport type { IRmuxBackend } from '../../types/rmuxBackend';\nimport type { RunnerRecord } from '../../types/runnerRegistry';\nimport {\n RUNNER_LOG_PARAMS,\n type RunnerLogResponse,\n type RunnerLogStreamEvent,\n type RunnerScreenEvent,\n} from '../../types/webRunnerLog';\n\n/** How often a follow re-reads the record to notice the runner exiting. */\n/**\n * How often a silent but living stream proves it is still there. Short enough\n * that an idle proxy does not reap the socket, long enough to cost nothing.\n */\n/** Kept in step with LogQuery's own default, so an unasked-for tail is the same size everywhere. */\n/** A page that asks for more than this gets this; the whole point of the route is a bounded read. */\n/** Path segments the client supplies are names, never paths. */\n/** Backends that expose a pane another process can read and write. */\nconst MULTIPLEXER_BACKENDS: ReadonlySet<string> = new Set(['rmux', 'tmux']);\n/** How often an attached pane's new bytes are picked up. */\n/** The unscrubbed copy the runner's sink writes beside the log, for interactive runs. */\n/** One keystroke batch; a paste is not a file transfer. */\n/** Said the same way for every reason, so the route never reports which check failed. */\n\nexport interface RunnerLogApiOptions {\n /** The session these routes answer for; the host owns exactly one. */\n sessionId: string;\n /** The store root; defaults to this environment's agent directory. */\n storeDir?: string;\n environment?: Readonly<Record<string, string | undefined>>;\n cwd?: string;\n logReader?: ILogReader;\n logTail?: ILogTail;\n /**\n * Reaches a live pane to read its screen and send it input.\n *\n * A session API is handed nothing but its session id, so it cannot touch the\n * in-process PTY handles the extension holds. This talks to the multiplexer\n * over its own socket instead, the same way the CLI's `input` verb does,\n * which is what makes an attached pane reachable across processes at all.\n */\n pane?: IRmuxBackend;\n}\n\n/** Rejects anything that could climb out of the session's own directory before a path is built. */\nfunction isSafeSegment(value: string): boolean {\n return value !== '' && value !== '.' && value !== '..' && SAFE_SEGMENT.test(value);\n}\n\n/**\n * The log file a run wrote, or undefined when there is no such run or its\n * record points somewhere it has no business pointing.\n *\n * The record names its own log path, and that record is written by another\n * process, so the path is checked rather than trusted: it must sit inside this\n * session's own logs directory. Without that check a doctored record would\n * turn this route into an arbitrary file read.\n */\nfunction resolveLogFile(stateDir: string, logsDir: string, sessionId: string, runId: string): RunnerRecord | undefined {\n const statePath = path.join(stateDir, `${runId}${STATE_EXTENSION}`);\n let raw: string;\n try {\n raw = fs.readFileSync(statePath, 'utf8');\n } catch {\n return undefined; // No such run in this session; the route answers 404.\n }\n const record = parseRunnerRecord(raw);\n if (record === undefined || record.id !== runId || record.sessionId !== sessionId) return undefined;\n const resolved = path.resolve(record.logPath);\n if (resolved !== logsDir && !resolved.startsWith(`${logsDir}${path.sep}`)) return undefined;\n return record;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction numberParam(value: string | undefined, fallback: number, max: number): number {\n const parsed = value === undefined ? Number.NaN : Number.parseInt(value, 10);\n if (!Number.isFinite(parsed) || parsed < 0) return fallback;\n return Math.min(parsed, max);\n}\n\nfunction logQueryOf(url: URL): LogQuery {\n const grep = url.searchParams.get(RUNNER_LOG_PARAMS.grep) ?? undefined;\n return {\n lines: numberParam(url.searchParams.get(RUNNER_LOG_PARAMS.lines) ?? undefined, DEFAULT_LINES, MAX_LINES),\n ...(grep === undefined || grep === '' ? {} : { grep }),\n ignoreCase: url.searchParams.get(RUNNER_LOG_PARAMS.ignoreCase) === 'true',\n contextLines: numberParam(url.searchParams.get(RUNNER_LOG_PARAMS.contextLines) ?? undefined, 0, MAX_LINES),\n };\n}\n\n/**\n * This package's HTTP surface: one runner's log, as a bounded slice and as a\n * live tail.\n *\n * The routes are mounted inside one session's own server, so they name a runner\n * and nothing else; the session is the host, not a parameter. The reader only\n * ever returns the last N lines of what matched, so both routes stay bounded no\n * matter how large the file on disk is.\n *\n * Paths come from src/types/apiRoutes, the one table the browser's generated\n * client reads too, so this app and the cockpit cannot disagree about where a\n * route lives. They stay relative to the mount the host strips.\n */\nexport function createRunnerLogApi(options: RunnerLogApiOptions): Hono {\n const logReader = options.logReader ?? new LogReader();\n const logTail = options.logTail ?? new LogTail();\n const sessionId = options.sessionId;\n const paths = new RunnerPaths(options.cwd, options.environment);\n const storeDir = options.storeDir ?? resolveRunnerStoreDirectory(options.environment ?? process.env, options.cwd);\n const stateDir =\n options.storeDir === undefined ? paths.stateDirectory(sessionId) : runnerStateDirFor(storeDir, sessionId);\n const logsDir =\n options.storeDir === undefined ? paths.logDirectory(sessionId) : path.join(storeDir, sessionId, LOG_DIR_NAME);\n const app = new Hono();\n\n // Built on first use, not on mount: most sessions never attach to a pane,\n // and constructing this probes the filesystem for a multiplexer binary.\n let paneBackend: IRmuxBackend | undefined = options.pane;\n const pane = (): IRmuxBackend => {\n paneBackend ??= (() => {\n const paths = new RunnerPaths(options.cwd, options.environment);\n return new PtyBackendChain(new RmuxBackend(paths), new TmuxBackend(paths));\n })();\n return paneBackend;\n };\n\n /** The record behind a request, or undefined when this session has no such run. */\n const runnerOf = (runId: string): RunnerRecord | undefined =>\n isSafeSegment(runId) ? resolveLogFile(stateDir, logsDir, sessionId, runId) : undefined;\n\n /**\n * The pane a request may attach to.\n *\n * Same posture as the log routes: the record is written by another process,\n * so nothing on it is trusted until it is checked. The target must be one\n * this package owns, the run must still be up, and it must have been started\n * interactive. A non-interactive run has no terminal to type into, and a\n * subprocess-backed run has no pane at all.\n */\n const attachableOf = (runId: string): { record: RunnerRecord; target: string } | undefined => {\n const record = runnerOf(runId);\n if (record === undefined || record.state !== RUNNING_STATE || record.interactive !== true) return undefined;\n const target = record.backendTarget;\n if (target === undefined || !MULTIPLEXER_BACKENDS.has(record.backend) || !isSafeSegment(target)) return undefined;\n return { record, target };\n };\n app.get(routes.log.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const record = runnerOf(runId);\n if (record === undefined) return context.json({ error: `No runner '${runId}' in this session.` }, 404);\n const slice = logReader.read(record.logPath, logQueryOf(new URL(context.req.url)));\n const body: RunnerLogResponse = { ...slice, runId, running: record.state === RUNNING_STATE };\n return context.json(body);\n });\n\n app.get(routes.logStream.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const record = runnerOf(runId);\n if (record === undefined) return context.json({ error: `No runner '${runId}' in this session.` }, 404);\n const from = numberParam(\n new URL(context.req.url).searchParams.get(RUNNER_LOG_PARAMS.from) ?? undefined,\n 0,\n Number.MAX_SAFE_INTEGER,\n );\n\n return streamSSE(context, async (stream) => {\n const send = (event: RunnerLogStreamEvent): Promise<void> =>\n stream.writeSSE({ event: RUNNER_LOG_STREAM_EVENT, data: JSON.stringify(event) });\n\n // The tail and the state poll both push onto one queue, so the stream\n // writes in the order things happened rather than interleaving two\n // writers onto the same socket.\n let pump: Promise<void> = Promise.resolve();\n const queue = (event: RunnerLogStreamEvent): void => {\n pump = pump.then(() => send(event)).catch(() => undefined);\n };\n\n let finish = (): void => undefined;\n const done = new Promise<void>((resolve) => {\n finish = resolve;\n });\n const handle = logTail.follow(record.logPath, {\n from,\n onLines: (lines, completeThrough) => queue({ lines, offset: completeThrough }),\n onError: () => finish(),\n });\n // A finished runner writes nothing more, so the page is told once and\n // the stream ends rather than holding a socket open forever.\n const poll = setInterval(() => {\n const current = runnerOf(runId);\n if (current !== undefined && current.state === RUNNING_STATE) return;\n queue({ lines: [], ended: true });\n finish();\n }, STATE_POLL_MS);\n // A runner can be alive and produce nothing for minutes. Without this the\n // page cannot tell that from a stream that quietly died.\n const heartbeat = setInterval(() => {\n pump = pump.then(() => stream.writeSSE({ event: RUNNER_LOG_PING_EVENT, data: '' })).catch(() => undefined);\n }, HEARTBEAT_MS);\n\n stream.onAbort(() => finish());\n await done;\n clearInterval(poll);\n clearInterval(heartbeat);\n handle.close();\n await pump;\n });\n });\n\n app.get(routes.screenStream.path, (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const attachable = attachableOf(runId);\n if (attachable === undefined) return context.json({ error: NOT_ATTACHABLE }, 404);\n // The record's log path is already proven to sit inside this session's own\n // logs directory, and the raw copy is that same path with a suffix, so it\n // cannot point anywhere new.\n const rawPath = `${attachable.record.logPath}${RAW_LOG_SUFFIX}`;\n const from = numberParam(\n new URL(context.req.url).searchParams.get(RUNNER_LOG_PARAMS.from) ?? undefined,\n 0,\n Number.MAX_SAFE_INTEGER,\n );\n\n return streamSSE(context, async (stream) => {\n let pump: Promise<void> = Promise.resolve();\n const queue = (event: RunnerScreenEvent): void => {\n pump = pump\n .then(() => stream.writeSSE({ event: RUNNER_SCREEN_EVENT, data: JSON.stringify(event) }))\n .catch(() => undefined);\n };\n\n let finish = (): void => undefined;\n const done = new Promise<void>((resolve) => {\n finish = resolve;\n });\n\n let offset = from;\n const drain = (): void => {\n let size: number;\n try {\n size = fs.statSync(rawPath).size;\n } catch {\n return; // The sink has not opened the file yet; the next tick settles it.\n }\n // The sink starts the file over when it passes its ceiling, so a\n // smaller file is a fresh one rather than a bad offset.\n if (size < offset) offset = 0;\n if (size === offset) return;\n const length = size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = fs.openSync(rawPath, 'r');\n let read: number;\n try {\n read = fs.readSync(descriptor, buffer, 0, length, offset);\n } finally {\n fs.closeSync(descriptor);\n }\n if (read <= 0) return;\n offset += read;\n // Bytes, not lines: a terminal's escape sequences straddle newlines and\n // mean nothing once split on them.\n queue({ chunk: buffer.subarray(0, read).toString('base64'), offset });\n };\n\n const poll = setInterval(() => {\n const current = runnerOf(runId);\n drain();\n if (current === undefined || current.state !== RUNNING_STATE) {\n queue({ chunk: '', offset, ended: true });\n finish();\n }\n }, SCREEN_POLL_MS);\n\n stream.onAbort(() => finish());\n await done;\n clearInterval(poll);\n await pump;\n });\n });\n\n /**\n * Types into a live pane.\n *\n * This is the one route in the package that writes rather than reads, so it\n * is deliberately narrow: an owned target, still running, started\n * interactive, and a body that is nothing but text.\n */\n app.post(routes.input.path, async (context) => {\n const runId = context.req.param(RUN_ID_PARAM);\n const attachable = attachableOf(runId);\n if (attachable === undefined) return context.json({ error: NOT_ATTACHABLE }, 404);\n\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'Expected a JSON body carrying text.' }, 400);\n }\n const text = isRecord(body) ? body.text : undefined;\n if (typeof text !== 'string' || text === '') return context.json({ error: 'Expected a non-empty text.' }, 400);\n if (text.length > MAX_INPUT_CHARS) return context.json({ error: 'That is more input than a pane takes.' }, 413);\n\n const delivered = await pane().input(attachable.target, text);\n if (!delivered) return context.json({ error: 'The runner did not take the input.' }, 502);\n return context.json({ delivered: true });\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built session entry. */\nexport const api: DoomApi = {\n basePath: RUNNER_API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n // The host is one session's server, so its id is the whole scope; a hub\n // would have handed no session at all, and there is nothing to answer.\n const app = createRunnerLogApi({\n sessionId: context.sessionId ?? '',\n cwd: context.cwd,\n environment: context.environment,\n });\n return {\n fetch: (request) => app.fetch(request),\n // Nothing outlives a request: a follow's watch and its poll are both\n // torn down when its own stream ends or the socket aborts.\n close: () => undefined,\n };\n },\n};\n","import { defineRoute } from '@agimon-ai/doompi-core/extensionFile';\n\nimport { api } from '../../../../../../services/runnerLogApi';\n\n/**\n * Mounts this package's HTTP API at `runner`, which the folder names.\n *\n * The folder is the declaration: the build reads the base path off it and hands\n * the browser a client bound to the same segment, so the mount is stated once\n * rather than in a constant and in four URL builders that were each free to\n * drift.\n */\nexport default defineRoute(api);\n","import { defineServerCommand, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { RunnerServerScope } from '../_lib/serverRoot';\nexport default defineServerCommand(\n (context: WithRoot<DoomServerPluginContext, RunnerServerScope>) => context.root.command,\n);\n","import { defineServerTool, type WithRoot } from '@agimon-ai/doompi-core/extensionFile';\nimport type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\n\nimport type { RunnerServerScope } from '../_lib/serverRoot';\nexport default defineServerTool((context: WithRoot<DoomServerPluginContext, RunnerServerScope>) => context.root.tool);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/doompi-core/serverFacet';\nimport { composeRootHooks } from '@agimon-ai/doompi-core/extensionFile';\n\nimport rootSession from '../src/extensions/workspaces/sessions/(backend)/root.server';\nimport channelRunnerRuns from '../src/extensions/(backend)/channel/runnerRuns.server';\nimport apiRoute from '../src/extensions/workspaces/sessions/(backend)/api/runner/route.server';\nimport commandRunners from '../src/extensions/workspaces/sessions/(backend)/command/runners.server';\nimport toolBash from '../src/extensions/workspaces/sessions/(backend)/tool/bash.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/doompi-runner',\n global: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n }),\n workspace: () => ({\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n }),\n session: async (context) => {\n const scopeSession = await rootSession(context);\n const contextSession = { ...context, root: scopeSession.value };\n return {\n ...composeRootHooks(scopeSession),\n get api(): DoomServerSessionPlugin['api'] { return [at(apiRoute, contextSession)]; },\n get channels(): DoomServerSessionPlugin['channels'] { return [() => via({ frameType: 'runner_runs' }, channelRunnerRuns())]; },\n get commands(): DoomServerSessionPlugin['commands'] { return [via({ name: 'runners' }, at(commandRunners, contextSession))]; },\n get tools(): DoomServerSessionPlugin['tools'] { return [via({ name: 'bash' }, at(toolBash, contextSession))]; },\n services: [...(scopeSession.services ?? [])],\n get activities(): DoomServerSessionPlugin['activities'] { return [...(scopeSession.activities ?? [])]; },\n };\n },\n});\n\nexport default facet;\n"],"mappings":"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"}
|
package/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Resources
|
|
4
4
|
|
|
5
|
-
- [Package README](./README.md): Supervised Bash behavior, CLI controls, storage, limits, and native artifacts.
|
|
5
|
+
- [Package README](./README.md): Supervised Bash behavior, CLI controls, storage, limits, completion handoff ownership, and native artifacts.
|
|
6
6
|
|
|
7
7
|
## How to use
|
|
8
8
|
|
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.126",
|
|
4
4
|
"description": "Supervised shell execution, background process control, and run logs for Pi coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -228,10 +228,10 @@
|
|
|
228
228
|
"registry": "https://registry.npmjs.org/"
|
|
229
229
|
},
|
|
230
230
|
"dependencies": {
|
|
231
|
-
"@agimon-ai/doompi-core": "0.0.1-alpha.
|
|
231
|
+
"@agimon-ai/doompi-core": "0.0.1-alpha.125",
|
|
232
232
|
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.104",
|
|
233
|
-
"@agimon-ai/doompi-ui": "0.0.1-alpha.
|
|
234
|
-
"@agimon-ai/doompi-web-components": "0.0.1-alpha.
|
|
233
|
+
"@agimon-ai/doompi-ui": "0.0.1-alpha.125",
|
|
234
|
+
"@agimon-ai/doompi-web-components": "0.0.1-alpha.83",
|
|
235
235
|
"@agimon-ai/doompi-web-security": "0.0.1-alpha.67",
|
|
236
236
|
"@agimon-ai/foundation-process-registry": "0.30.8",
|
|
237
237
|
"@deepseek-ai/cordis": "4.0.4",
|