# Conflicts: # apps/web/tests/expected/agent-preset-selection/header.expected.md # docs/architecture.i18n.yaml # docs/config-catalog.i18n.yaml # docs/config-catalog.md # docs/event-producer-consumer.i18n.yaml # docs/event-producer-consumer.md # docs/event-producer-consumer.zh.md # docs/module-graph.i18n.yaml # docs/module-graph.md # docs/module-graph.zh.md # docs/persistence-catalog.i18n.yaml # docs/persistence-catalog.md # docs/persistence-catalog.zh.md # docs/persistence-schema.json # docs/subsystems/web-client.i18n.yaml # packages/api/README.i18n.yaml # packages/api/README.md # packages/api/README.zh.md # packages/api/session-controller/README.i18n.yaml # packages/api/session-controller/README.md # packages/api/session-controller/README.zh.md # packages/api/session-controller/src/client/sessions/manager.ts # packages/api/session-controller/src/client/sessions/service.ts # packages/api/session-controller/tests/manager.client.spec.ts # packages/client/locale/tests/language-row.client.spec.tsx # packages/client/ui-chat/tests/chat-view.client.spec.tsx # packages/client/ui-chat/tests/transcript-view-row.client.spec.tsx # packages/client/ui-conversation/tests/conversation-registry.client.spec.ts # packages/client/ui-conversation/tests/enter-behavior-row.client.spec.tsx # packages/client/ui-conversation/tests/input-bar.client.spec.tsx # packages/client/ui-conversation/tests/input-matrix.client.spec.tsx # packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx # packages/client/ui-conversation/tests/skeleton.client.spec.tsx # packages/client/ui-deliverables/tests/deliverables.client.spec.tsx # packages/client/ui-jobs/tests/job-list-action.client.spec.tsx # packages/client/ui-layout/tests/app-frame.client.spec.tsx # packages/client/ui-layout/tests/document-title.client.spec.tsx # packages/client/ui-open-in-app/tests/open-in-app-action.client.spec.tsx # packages/client/ui-primitives/README.i18n.yaml # packages/client/ui-session/tests/ui-session.client.spec.ts # packages/client/ui-settings-general/tests/settings-root.client.spec.tsx # packages/client/ui-settings-unarchive-sessions/tests/components.client.spec.tsx # packages/client/ui-subagent/README.i18n.yaml # packages/client/ui-subagent/README.md # packages/client/ui-subagent/README.zh.md # packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx # packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx # packages/client/ui-theme/tests/appearance-row.client.spec.tsx # packages/client/ui-theme/tests/font-size-row.client.spec.tsx # packages/client/ui-tool/tests/coverage-tails.client.spec.tsx # packages/client/ui-tool/tests/diff-card.client.spec.tsx # packages/client/ui-tool/tests/image-card.client.spec.tsx # packages/client/ui-tool/tests/read-card.client.spec.tsx # packages/client/ui-tool/tests/terminal-card.client.spec.tsx # packages/client/ui-trajectory/tests/views.client.spec.tsx # packages/client/ui-user-questions/tests/plan-review-panel.client.spec.tsx # packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx # packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx # packages/client/ui-workspace/tests/apply.client.spec.ts # packages/client/ui-workspace/tests/tree.client.spec.ts # packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx # packages/client/ui-workspace/tests/workspace-picker.client.spec.tsx # packages/client/ui-workspace/tests/workspaces-service.client.spec.ts # packages/extensions/cordis-client-runner/src/client/slot-catalog.ts # packages/jobs/tool-jobs/src/index.ts # packages/shell/tool-bash/README.i18n.yaml # packages/test-support/client-runtime/src/sessions.ts
113 lines
4.9 KiB
TypeScript
113 lines
4.9 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 { JobHooks, 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(' ')}` }
|
|
}
|
|
|
|
/**
|
|
* The process's non-consuming stream readers as registry pull sources. 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 `spawn failed: …` note.
|
|
* @param proc - the started process's observed streams, once the starter has spawned it.
|
|
* @returns one source per stream, stdout first.
|
|
*/
|
|
export function processSources(proc: () => Pick<ShellProcess, 'observed'> | undefined): JobOutputSource[] {
|
|
const source = (channel: 'stdout' | 'stderr'): JobOutputSource => ({
|
|
channel,
|
|
read: (fromByte) => {
|
|
const live = proc()
|
|
return live === undefined ? { text: '', nextOffset: fromByte, lossy: false } : live.observed[channel].readFrom(fromByte)
|
|
},
|
|
})
|
|
return [source('stdout'), source('stderr')]
|
|
}
|
|
|
|
/**
|
|
* Adapt asynchronous shell preparation after job admission without exposing a partial process.
|
|
* @param start - starts the process with job-owned cancellation.
|
|
* @param outcome - projects the settled process into the job outcome.
|
|
* @returns synchronous job hooks whose completion includes preparation and process settlement.
|
|
*/
|
|
export function processJob(
|
|
start: (signal: AbortSignal) => Promise<ShellProcess>,
|
|
outcome: (process: ShellProcess) => JobOutcome,
|
|
): JobHooks {
|
|
const controller = new AbortController()
|
|
let process: ShellProcess | undefined
|
|
const done: Promise<JobOutcome> = (async () => {
|
|
try {
|
|
process = await start(controller.signal)
|
|
try {
|
|
if (controller.signal.aborted) process.kill()
|
|
} finally {
|
|
await process.done
|
|
}
|
|
return outcome(process)
|
|
} catch (error: unknown) {
|
|
return {
|
|
status: controller.signal.aborted && process === undefined ? 'killed' : 'failed',
|
|
detail: error instanceof Error ? error.message : String(error),
|
|
}
|
|
}
|
|
})()
|
|
return {
|
|
cancel: (reason) => {
|
|
if (controller.signal.aborted) return
|
|
controller.abort(reason)
|
|
process?.kill()
|
|
},
|
|
done,
|
|
}
|
|
}
|