# Conflicts: # packages/shell/bash-local/README.i18n.yaml # packages/shell/bash-local/README.md # packages/shell/bash-local/README.zh.md # packages/shell/bash-local/src/index.ts # packages/shell/bash-local/tests/executor.spec.ts # packages/shell/pwsh-local/README.i18n.yaml # packages/shell/pwsh-local/README.md # packages/shell/pwsh-local/README.zh.md # packages/shell/pwsh-local/src/index.ts # packages/shell/shell/README.i18n.yaml # packages/shell/shell/README.md # packages/shell/shell/README.zh.md # snapshots/session/pwsh-tool-turn/tool-schemas.expected.json
99 lines
4.6 KiB
TypeScript
99 lines
4.6 KiB
TypeScript
/**
|
|
* Generic-job adaptation for background bash process handles: the terminal
|
|
* outcome the registry records and the pull sources it pumps.
|
|
*
|
|
* @module @deepseek-ai/dsh-tool-bash/background
|
|
*/
|
|
|
|
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
|
import { escalationHintMarker, sandboxDenialMarker } from '@deepseek-ai/dsh-sandbox'
|
|
import type { ShellProcess, ShellSandboxInfo } from '@deepseek-ai/dsh-shell'
|
|
import type { JobOutcome, JobOutputSource } from '@deepseek-ai/dsh-jobs'
|
|
|
|
/**
|
|
* Sandbox facts worth the terminal detail: a runner that never ran the
|
|
* command, or a denial (with the escalation hint this composition offers).
|
|
* @param sandbox - settled sandbox facts, when this was a confined process.
|
|
* @param escalationModes - escalation targets advertised by this composition.
|
|
* @returns the markers to append, oldest first.
|
|
*/
|
|
function sandboxNotes(sandbox: ShellSandboxInfo | undefined, escalationModes: readonly SandboxMode[]): string[] {
|
|
if (sandbox?.runnerFailed) {
|
|
return [`[sandbox: the sandbox runner itself failed under ${sandbox.mode} mode — the command did not run; this is a sandbox problem, not a command failure]`]
|
|
}
|
|
if (sandbox?.denied) {
|
|
const notes = [sandboxDenialMarker(sandbox.mode)]
|
|
if (escalationModes.length > 0) notes.push(escalationHintMarker('command'))
|
|
return notes
|
|
}
|
|
return []
|
|
}
|
|
|
|
/**
|
|
* Map a settled background process onto the generic job-outcome vocabulary:
|
|
* `killed` stays `killed` (detail: the signal when one is known), everything
|
|
* else is `completed` with the exit code as detail. A nonzero command exit is
|
|
* reported, not failed, exactly like the foreground rendering. Sandbox facts
|
|
* join the detail, since a job's terminal reason is the one line every
|
|
* reader — the model's status line, the roster row — shows.
|
|
* @param proc - the settled process handle.
|
|
* @param escalationModes - escalation targets advertised by this composition.
|
|
* @returns the outcome for the `ctx.jobs` registration.
|
|
*/
|
|
export function processOutcome(proc: ShellProcess, escalationModes: readonly SandboxMode[] = []): JobOutcome {
|
|
// TODO(background-infrastructure-outcome): widen ShellProcess with an explicit
|
|
// infrastructure-failure outcome, then map it to job `failed`. Restricted
|
|
// runner failures expose sandbox.runnerFailed, but unconfined spawn failures
|
|
// still alias a signal-less kill; real nonzero command exits must remain
|
|
// `completed`.
|
|
const base: JobOutcome = proc.status === 'killed'
|
|
? { status: 'killed', detail: proc.signal !== null ? `signal: ${proc.signal}` : 'killed before exit' }
|
|
: { status: 'completed', detail: `exit code: ${proc.exitCode ?? 0}` }
|
|
const notes = sandboxNotes(proc.sandbox, escalationModes)
|
|
return notes.length === 0 ? base : { ...base, detail: `${base.detail}; ${notes.join(' ')}` }
|
|
}
|
|
|
|
/** Whole-stream offsets of a process's observed streams. */
|
|
export interface ObservedOffsets {
|
|
readonly stdout: number
|
|
readonly stderr: number
|
|
}
|
|
|
|
/**
|
|
* The current end of each observed stream: where a pull source starts so the
|
|
* ring holds only what the model has not already been handed.
|
|
* @param proc - the running process.
|
|
* @returns the per-stream end offsets.
|
|
*/
|
|
export function observedOffsets(proc: ShellProcess): ObservedOffsets {
|
|
const end = (channel: 'stdout' | 'stderr'): number => proc.observed[channel].readFrom(0).nextOffset
|
|
return { stdout: end('stdout'), stderr: end('stderr') }
|
|
}
|
|
|
|
/**
|
|
* The process's non-consuming stream readers as registry pull sources, each
|
|
* starting at `from`: 0 for a fresh background run, the offsets a promotion
|
|
* already handed the model for a promoted one. They bind lazily because the
|
|
* process is spawned inside the starter, after the registry admitted the job;
|
|
* a read before the spawn yields nothing, and the pump keeps the model's
|
|
* consuming cursor untouched. A rejected spawn's stderr reader carries the
|
|
* provider's `subprocess failed before reporting an outcome: …` note.
|
|
* @param proc - the started process, once the starter has spawned it.
|
|
* @param from - per-stream offsets the sources start pulling at.
|
|
* @returns one source per stream, stdout first.
|
|
*/
|
|
export function processSources(
|
|
proc: () => ShellProcess | undefined,
|
|
from: ObservedOffsets = { stdout: 0, stderr: 0 },
|
|
): JobOutputSource[] {
|
|
const source = (channel: 'stdout' | 'stderr'): JobOutputSource => ({
|
|
channel,
|
|
read: (fromByte) => {
|
|
const start = Math.max(fromByte, from[channel])
|
|
const live = proc()
|
|
return live === undefined ? { text: '', nextOffset: start, lossy: false } : live.observed[channel].readFrom(start)
|
|
},
|
|
})
|
|
return [source('stdout'), source('stderr')]
|
|
}
|