background.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. /**
  2. * Generic-task adaptation for background bash process handles.
  3. *
  4. * @module @deepseek-ai/dsh-tool-bash/background
  5. */
  6. import type { ShellProcess } from '@deepseek-ai/dsh-shell'
  7. import type { JobHooks, JobOutcome } from '@deepseek-ai/dsh-jobs'
  8. /**
  9. * Map a settled background process onto the generic task-outcome vocabulary:
  10. * `killed` stays `killed` (detail: the signal when one is known), everything
  11. * else is `completed` with the exit code as detail. A nonzero command exit is
  12. * reported, not failed, exactly like the foreground rendering.
  13. * @param proc - the settled process handle.
  14. * @returns the outcome for the `ctx.jobs` registration.
  15. */
  16. export function processOutcome(proc: ShellProcess): { status: 'completed' | 'killed'; detail: string } {
  17. // TODO(background-infrastructure-outcome): widen ShellProcess with an explicit
  18. // infrastructure-failure outcome, then map it to task `failed`. Restricted
  19. // runner failures expose sandbox.runnerFailed, but unconfined spawn failures
  20. // still alias a signal-less kill; real nonzero command exits must remain
  21. // `completed`.
  22. if (proc.status === 'killed') {
  23. return { status: 'killed', detail: proc.signal !== null ? `signal: ${proc.signal}` : 'killed before exit' }
  24. }
  25. return { status: 'completed', detail: `exit code: ${proc.exitCode ?? 0}` }
  26. }
  27. /**
  28. * Adapt asynchronous shell preparation after job admission without exposing a partial process.
  29. * @param start - starts the process with job-owned cancellation.
  30. * @param renderOutput - consumes output from a published process.
  31. * @returns synchronous job hooks whose completion includes preparation and process settlement.
  32. */
  33. export function processJob(
  34. start: (signal: AbortSignal) => Promise<ShellProcess>,
  35. renderOutput: (process: ShellProcess) => string,
  36. ): JobHooks {
  37. const controller = new AbortController()
  38. let process: ShellProcess | undefined
  39. const done: Promise<JobOutcome> = (async () => {
  40. try {
  41. process = await start(controller.signal)
  42. try {
  43. if (controller.signal.aborted) process.kill()
  44. } finally {
  45. await process.done
  46. }
  47. return processOutcome(process)
  48. } catch (error: unknown) {
  49. return {
  50. status: controller.signal.aborted && process === undefined ? 'killed' : 'failed',
  51. detail: error instanceof Error ? error.message : String(error),
  52. }
  53. }
  54. })()
  55. return {
  56. cancel: (reason) => {
  57. if (controller.signal.aborted) return
  58. controller.abort(reason)
  59. process?.kill()
  60. },
  61. done,
  62. readOutput: () => process === undefined ? '' : renderOutput(process),
  63. }
  64. }