mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subprocess/README.md
|
||||
README.md: 23c8ba44916379c032991bc92df557157c2e66c4
|
||||
README.zh.md: 919aaacf32e2bc2de4c84393d67e2b5214ce5f06
|
||||
README.md: b92dfe0d275f4b59e42ea1e86ad4e6482d89aeb6
|
||||
README.zh.md: 8cef3023a4d699a4b2c1d1d6a747737acb4ff199
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The shared process substrate for one execution world: executable lookup, fully-specified managed child-process trees with raw or collected stdio, and one deep terminal-process primitive that owns PTY allocation, foreground groups, and provider-observable session cleanup. Command defaulting, shell semantics, deadlines, protocol framing, readiness, and presentation stay with consumers — the [bash executors](../bash/README.md), [LSP host](../lsp/README.md), [PTY shell backend](../pty/README.md), and [ACP subagent backend](../subagent/README.md). See the [subprocess seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md).
|
||||
The shared process substrate for one execution world: executable lookup, fully-specified managed child-process trees with raw or collected stdio, and one deep terminal-process primitive that owns PTY allocation, foreground groups, and provider-observable session cleanup. Command defaulting, shell semantics, deadlines, protocol framing, readiness, and presentation stay with consumers — the [bash executors](../shell/README.md), [LSP host](../lsp/README.md), [PTY shell backend](../terminal/README.md), and [ACP subagent backend](../subagent/README.md). See the [subprocess seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md).
|
||||
|
||||
| Package | ctx key | Role |
|
||||
|---|---|---|
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
这里集中提供一个执行世界的共享进程基底:可执行文件查找、具有原始或收集式 stdio 的完全明确指定的受管子进程树,以及一项底层终端进程原语,负责 PTY 分配、前台进程组和提供方仍可观察到的会话成员清理。命令默认值补全、shell 语义、时限、协议分帧、就绪状态与呈现留在消费方:[bash 执行器](../bash/README.md)、[LSP 主机](../lsp/README.md)、[PTY shell 后端](../pty/README.md)与 [ACP(Agent Client Protocol)subagent 后端](../subagent/README.md)。参见 [subprocess seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md)。
|
||||
这里集中提供一个执行世界的共享进程基底:可执行文件查找、具有原始或收集式 stdio 的完全明确指定的受管子进程树,以及一项底层终端进程原语,负责 PTY 分配、前台进程组和提供方仍可观察到的会话成员清理。命令默认值补全、shell 语义、时限、协议分帧、就绪状态与呈现留在消费方:[bash 执行器](../shell/README.md)、[LSP 主机](../lsp/README.md)、[PTY shell 后端](../terminal/README.md)与 [ACP(Agent Client Protocol)subagent 后端](../subagent/README.md)。参见 [subprocess seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md)。
|
||||
|
||||
| 包 | ctx 键 | 角色 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subprocess/subprocess-local/README.md
|
||||
README.md: bf0af8779f0cc3e2c20382db40be4715814e78f4
|
||||
README.zh.md: 6d0e102d9b4c55049106f937b6f04ec26a458cb6
|
||||
README.md: 645f05a0daf92d64c438f8aaae3b0e115f253a64
|
||||
README.zh.md: cd8c04071f00b298b2d242624c2ad1327758e16e
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Local Service provider for the [`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam. `LocalSubprocessService` resolves local executables, spawns ordinary detached process trees with explicit stdio, and implements terminal processes through `node-pty` plus platform process inspection. It has no config: every disposition, limit, terminal dimension, grace, and directory arrives from the calling capability seams ([`dsh-bash-local`](../../bash/bash-local/README.md), [`dsh-lsp-local`](../../lsp/lsp-local/README.md), and [`dsh-pty-local`](../../pty/pty-local/README.md)).
|
||||
Local Service provider for the [`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam. `LocalSubprocessRuntime` resolves local executables, spawns ordinary detached process trees with explicit stdio, and implements terminal processes through `node-pty` plus platform process inspection. It has no config: every disposition, limit, terminal dimension, grace, and directory arrives from the calling capability seams ([`dsh-bash-local`](../../shell/bash-local/README.md), [`dsh-lsp-stdio`](../../lsp/lsp-stdio/README.md), and [`dsh-terminal-bash`](../../terminal/terminal-bash/README.md)).
|
||||
|
||||
## Behavior
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
[`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam 的本地 Service provider。`LocalSubprocessService` 解析本地可执行文件,以显式 stdio spawn 普通 detached 进程树,并通过 `node-pty` 加平台进程检查实现终端进程。该实现没有任何配置:每项处置方式、限制、终端尺寸、宽限期与目录都来自调用方能力 seam([`dsh-bash-local`](../../bash/bash-local/README.md)、[`dsh-lsp-local`](../../lsp/lsp-local/README.md) 和 [`dsh-pty-local`](../../pty/pty-local/README.md))。
|
||||
[`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam 的本地 Service provider。`LocalSubprocessRuntime` 解析本地可执行文件,以显式 stdio spawn 普通 detached 进程树,并通过 `node-pty` 加平台进程检查实现终端进程。该实现没有任何配置:每项处置方式、限制、终端尺寸、宽限期与目录都来自调用方能力 seam([`dsh-bash-local`](../../shell/bash-local/README.md)、[`dsh-lsp-stdio`](../../lsp/lsp-stdio/README.md) 和 [`dsh-terminal-bash`](../../terminal/terminal-bash/README.md))。
|
||||
|
||||
## 行为
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { delimiter, extname, isAbsolute, resolve } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import * as nodePty from 'node-pty'
|
||||
import type { IPtyForkOptions } from 'node-pty'
|
||||
import { SubprocessService } from '@deepseek-ai/dsh-subprocess'
|
||||
import { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess'
|
||||
import type {
|
||||
SubprocessHandle,
|
||||
SubprocessSpawnSpec,
|
||||
@@ -34,7 +34,7 @@ import { LocalTerminalHandle } from './terminal.ts'
|
||||
* SIGTERM→grace→SIGKILL escalation, plus synchronous final termination during
|
||||
* JavaScript-observable host exit.
|
||||
*/
|
||||
export class LocalSubprocessService extends SubprocessService {
|
||||
export class LocalSubprocessRuntime extends SubprocessRuntime {
|
||||
/** Live handles retained for normal disposal and synchronous host-exit finalization. */
|
||||
private live = new Set<LocalSubprocessHandle>()
|
||||
/** Live terminals retained through normal quiescence or host-exit finalization. */
|
||||
@@ -192,4 +192,4 @@ function environmentValue(env: NodeJS.ProcessEnv, name: 'PATH' | 'PATHEXT'): str
|
||||
return Object.entries(env).find(([key]) => key.toUpperCase() === normalized)?.[1]
|
||||
}
|
||||
|
||||
export default LocalSubprocessService
|
||||
export default LocalSubprocessRuntime
|
||||
|
||||
@@ -2,7 +2,7 @@ import { access, readFile, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
const [kind, trigger, root] = process.argv.slice(2)
|
||||
if ((kind !== 'ordinary' && kind !== 'terminal')
|
||||
@@ -30,7 +30,7 @@ async function waitForFile(path: string): Promise<void> {
|
||||
|
||||
const listenersBefore = process.listenerCount('exit')
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const listenersAfterLoad = process.listenerCount('exit')
|
||||
if (kind === 'ordinary') {
|
||||
ctx.subprocess.spawn({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PassThrough } from 'node:stream'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { basename, dirname, relative, resolve } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
import type { SubprocessSpawnSpec, SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
||||
import { childEnv } from '../src/spawn.ts'
|
||||
|
||||
@@ -20,13 +20,13 @@ function spec(command: string, overrides: Partial<SubprocessSpawnSpec> = {}): Su
|
||||
}
|
||||
}
|
||||
|
||||
describe('LocalSubprocessService', () => {
|
||||
describe('LocalSubprocessRuntime', () => {
|
||||
it('places the host-exit finalizer before listeners that predate the service', async () => {
|
||||
const baseline = new Set(process.listeners('exit'))
|
||||
const prior = vi.fn()
|
||||
process.on('exit', prior)
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
try {
|
||||
const listeners = process.listeners('exit')
|
||||
const finalizer = listeners.find(candidate => !baseline.has(candidate) && candidate !== prior)
|
||||
@@ -41,7 +41,7 @@ describe('LocalSubprocessService', () => {
|
||||
it('keeps the host-exit finalizer active until normal disposal reaches quiescence', async () => {
|
||||
const before = new Set(process.listeners('exit'))
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const listener = process.listeners('exit').find(candidate => !before.has(candidate))
|
||||
expect(listener).toBeTypeOf('function')
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('LocalSubprocessService', () => {
|
||||
it('contains each host-exit termination failure and continues with the other targets', async () => {
|
||||
const before = new Set(process.listeners('exit'))
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const listener = process.listeners('exit').find(candidate => !before.has(candidate))
|
||||
expect(listener).toBeTypeOf('function')
|
||||
const ordinaryFailure = vi.fn(() => { throw new Error('ordinary failed') })
|
||||
@@ -111,7 +111,7 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('resolves absolute and PATH executables and honors lookup cancellation', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
expect(await ctx.subprocess.resolveExecutable(process.execPath)).toBe(process.execPath)
|
||||
expect(await ctx.subprocess.resolveExecutable(basename(process.execPath), {
|
||||
PATH: dirname(process.execPath),
|
||||
@@ -137,8 +137,8 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('builds Windows executable candidates with case-insensitive overrides', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const service = ctx.subprocess as LocalSubprocessService
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const service = ctx.subprocess as LocalSubprocessRuntime
|
||||
const candidates = (service as unknown as {
|
||||
executableCandidates(command: string, env: NodeJS.ProcessEnv): string[]
|
||||
}).executableCandidates.bind(service)
|
||||
@@ -163,7 +163,7 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('validates terminal allocation inputs before allocating a PTY', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const base: SubprocessTerminalSpawnSpec = {
|
||||
argv: ['bash'], cwd: process.cwd(), rows: 24, cols: 80, graceMs: 10,
|
||||
}
|
||||
@@ -175,7 +175,7 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('terminates and joins an owned terminal during disposal', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const terminate = vi.fn(async () => {})
|
||||
const terminal: SubprocessTerminalHandle = {
|
||||
pid: 1,
|
||||
@@ -195,7 +195,7 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('waits for every terminal cleanup and aggregates teardown failures', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const service = ctx.subprocess
|
||||
const firstFailure = new Error('first cleanup failure')
|
||||
const secondFailure = new Error('second cleanup failure')
|
||||
@@ -246,7 +246,7 @@ describe('LocalSubprocessService', () => {
|
||||
const failure = new Error('single cleanup failure')
|
||||
const disposalErrors: unknown[] = []
|
||||
ctx.logger.error = ((error: unknown) => { disposalErrors.push(error) }) as typeof ctx.logger.error
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const service = ctx.subprocess
|
||||
const terminal: SubprocessTerminalHandle = {
|
||||
pid: 1,
|
||||
@@ -268,7 +268,7 @@ describe('LocalSubprocessService', () => {
|
||||
it('force-terminates remaining targets before releasing a failed disposal', async () => {
|
||||
const before = new Set(process.listeners('exit'))
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const listener = process.listeners('exit').find(candidate => !before.has(candidate))
|
||||
expect(listener).toBeTypeOf('function')
|
||||
const failure = new Error('cleanup failed')
|
||||
@@ -317,10 +317,10 @@ describe('LocalSubprocessService', () => {
|
||||
createProcessInspector: () => inspector,
|
||||
}))
|
||||
try {
|
||||
const { default: IsolatedLocalSubprocessService } = await import('../src/index.ts')
|
||||
const { default: IsolatedLocalSubprocessRuntime } = await import('../src/index.ts')
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(IsolatedLocalSubprocessService)
|
||||
const service = ctx.subprocess as InstanceType<typeof IsolatedLocalSubprocessService>
|
||||
const fiber = await ctx.plugin(IsolatedLocalSubprocessRuntime)
|
||||
const service = ctx.subprocess as InstanceType<typeof IsolatedLocalSubprocessRuntime>
|
||||
const handle = await ctx.subprocess.spawnTerminal({
|
||||
argv: ['shell'], cwd: process.cwd(), rows: 24, cols: 80, graceMs: 1,
|
||||
})
|
||||
@@ -352,13 +352,13 @@ describe('LocalSubprocessService', () => {
|
||||
vi.resetModules()
|
||||
vi.doMock('node-pty', () => ({ spawn: () => terminal }))
|
||||
try {
|
||||
const { default: IsolatedLocalSubprocessService } = await import('../src/index.ts')
|
||||
const { default: IsolatedLocalSubprocessRuntime } = await import('../src/index.ts')
|
||||
const ctx = new Context()
|
||||
const disposalErrors: unknown[] = []
|
||||
ctx.logger.error = ((error: unknown) => { disposalErrors.push(error) }) as typeof ctx.logger.error
|
||||
const fiber = await ctx.plugin(IsolatedLocalSubprocessService)
|
||||
const fiber = await ctx.plugin(IsolatedLocalSubprocessRuntime)
|
||||
const alive = new Set([124])
|
||||
;(ctx.subprocess as InstanceType<typeof IsolatedLocalSubprocessService>).terminalInspector = {
|
||||
;(ctx.subprocess as InstanceType<typeof IsolatedLocalSubprocessRuntime>).terminalInspector = {
|
||||
foregroundPgid: () => 123,
|
||||
isStdinWaiting: () => false,
|
||||
processTree: () => [{ pid: 123, started: 'shell' }, { pid: 124, started: 'child' }],
|
||||
@@ -384,7 +384,7 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('registers as ctx.subprocess and spawns managed handles', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const handle = ctx.subprocess.spawn(spec('echo managed'))
|
||||
const result = await handle.done
|
||||
expect(result.exitCode).toBe(0)
|
||||
@@ -394,7 +394,7 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('disposal kills still-running processes and awaits their exit', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const handle = ctx.subprocess.spawn(spec('sleep 60'))
|
||||
await fiber.dispose()
|
||||
const outcome = await handle.done
|
||||
@@ -403,7 +403,7 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('a settled process leaves the live set (disposal does not re-kill it)', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const handle = ctx.subprocess.spawn(spec('true'))
|
||||
const outcome = await handle.done
|
||||
expect(outcome.exitCode).toBe(0)
|
||||
@@ -412,7 +412,7 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('disposal tolerates a handle whose spawn already failed', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
const handle = ctx.subprocess.spawn(spec('true', { cwd: '/nonexistent-dir-dsh-subprocess-test' }))
|
||||
await expect(handle.done).rejects.toThrow()
|
||||
await fiber.dispose()
|
||||
@@ -420,7 +420,7 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('disposal contains a spawn-failure rejection that races teardown', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
// Dispose before the rejection continuation removes the handle from the
|
||||
// live set, so teardown itself must swallow the rejected done.
|
||||
const handle = ctx.subprocess.spawn(spec('true', { cwd: '/nonexistent-dir-dsh-subprocess-test' }))
|
||||
@@ -430,8 +430,8 @@ describe('LocalSubprocessService', () => {
|
||||
|
||||
it('loading a second implementation throws (one processes service per context — cordis standard)', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
class SecondManager extends LocalSubprocessService {}
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
class SecondManager extends LocalSubprocessRuntime {}
|
||||
await expect(ctx.plugin(SecondManager)).rejects.toThrow(/service "subprocess" has been registered/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -706,10 +706,10 @@ describe('tree-survivor escalation (terminate and bounded waits reach helpers th
|
||||
|
||||
it('service teardown awaits tree survivors, not just handle settlement', async () => {
|
||||
const { Context } = await import('@deepseek-ai/cordis')
|
||||
const { default: LocalSubprocessService } = await import('@deepseek-ai/dsh-subprocess-local')
|
||||
const { default: LocalSubprocessRuntime } = await import('@deepseek-ai/dsh-subprocess-local')
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
;(ctx.subprocess as InstanceType<typeof LocalSubprocessService>).internals = { spillDir }
|
||||
const fiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
;(ctx.subprocess as InstanceType<typeof LocalSubprocessRuntime>).internals = { spillDir }
|
||||
const pidFile = join(spillDir, `survivor-svc-${Date.now()}.pid`)
|
||||
const running = ctx.subprocess.spawn(spec(
|
||||
`bash -c 'trap "" TERM; echo $$ > ${pidFile}; sleep 60' >/dev/null 2>&1 & disown; exit 0`,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"path": "../../util/timeout"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subprocess/subprocess/README.md
|
||||
README.md: 71ed60c6bdbecbddf45ee4c6a3d31e4a3fbb479b
|
||||
README.zh.md: 3803598cd113e8fb2867273e14a0f2746e57eef7
|
||||
README.md: 5e80bc7205c2f06b3528bbcfcb775941d2fa4545
|
||||
README.zh.md: 62a8b0704d73bc433888f841273505fc92748fa9
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The subprocess seam (`ctx.subprocess`) is the process half of one execution world. The abstract `SubprocessService` exposes executable lookup, ordinary managed `spawn`, and one terminal-process primitive; its vocabulary covers raw/collected stdio, process and terminal handles, exit facts, tree/session cleanup, and the managed `DSH_*` environment namespace. The local implementation lives in [`dsh-subprocess-local`](../subprocess-local/README.md).
|
||||
The subprocess seam (`ctx.subprocess`) is the process half of one execution world. The abstract `SubprocessRuntime` exposes executable lookup, ordinary managed `spawn`, and one terminal-process primitive; its vocabulary covers raw/collected stdio, process and terminal handles, exit facts, tree/session cleanup, and the managed `DSH_*` environment namespace. The local implementation lives in [`dsh-subprocess-local`](../subprocess-local/README.md).
|
||||
|
||||
## Contract
|
||||
|
||||
- `spawn(spec)` returns immediately with a live handle; `done` resolves at process close with exit facts (`SubprocessOutcome` carries no output and no cause classification) and rejects only for spawn-level failures.
|
||||
- Spawn working directories and executable paths belong to the provider's execution world. `resolveExecutable(command, env?, signal?)` verifies absolute commands or resolves bare names against that world's scrubbed PATH plus explicit overrides.
|
||||
- The spec is fully explicit — argv, cwd, per-stream stdio dispositions, grace — because deployment-varying defaults belong to the caller's config, not to a hidden subprocess-service default (the `dsh-bash` request/spec split is the owning template). `argv` is never shell-interpreted; a consumer that wants a shell passes `['bash', '-c', command]` itself.
|
||||
- The spec is fully explicit — argv, cwd, per-stream stdio dispositions, grace — because deployment-varying defaults belong to the caller's config, not to a hidden subprocess-service default (the `dsh-shell` request/spec split is the owning template). `argv` is never shell-interpreted; a consumer that wants a shell passes `['bash', '-c', command]` itself.
|
||||
- Stdio is Node-shaped per stream: `'pipe'` hands the caller the raw stream for its own protocol framing (LSP JSON-RPC, ACP ndjson), `'inherit'` passes the parent descriptor through for diagnostics, and collect mode (`{ maxBytes, spill? }`) buffers a bounded tail with an optional full-stream spill file. Collect readers take whole-stream byte offsets and never consume, so independent readers cannot steal one another's deltas; a read whose offset slid out of the in-memory tail is `lossy` and points at the spill file when one exists. Collected output stays readable after settlement.
|
||||
- Termination is tree-scoped on every platform (POSIX detached groups with direct-child fallback; Windows `taskkill /T`): `terminate()` — the only termination verb — escalates SIGTERM→grace→SIGKILL (idempotent, driven by the spec's abort signal too, a no-op once the tree is gone), and `waitForExit(signal?)` observes whole-tree liveness so a consumer-owned teardown ladder holds each tier on real quiescence — the manager reacts but never classifies why (callers own deadlines, teardown ladders, and cause classification).
|
||||
- `spawnTerminal(spec)` is the only non-pipe primitive. Its handle owns a real PTY, UTF-8 text I/O, foreground-process-group inspection/signalling, and one awaited `terminate()` operation that reaches quiescence for every session member the provider can still observe and settles in-flight handle calls; providers document substrate-specific observability limits. The spec signal cancels allocation only; the published handle owns its lifetime. The output stream ends after queued output when the top-level process exits, and a live transport failure rejects `done`. These operations remain one substrate primitive because ordinary pipes cannot allocate a controlling terminal or clean terminal-session members; readiness, scrollback, and owner policy remain in the PTY consumer.
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
子进程 seam(`ctx.subprocess`)是一个执行世界的进程部分。抽象的 `SubprocessService` 公开可执行文件查找、普通受管 `spawn` 和一项终端进程原语;其词汇涵盖原始/收集式 stdio、进程与终端句柄、退出事实、进程树/会话清理,以及受管的 `DSH_*` 环境命名空间。本地实现位于 [`dsh-subprocess-local`](../subprocess-local/README.md)。
|
||||
子进程 seam(`ctx.subprocess`)是一个执行世界的进程部分。抽象的 `SubprocessRuntime` 公开可执行文件查找、普通受管 `spawn` 和一项终端进程原语;其词汇涵盖原始/收集式 stdio、进程与终端句柄、退出事实、进程树/会话清理,以及受管的 `DSH_*` 环境命名空间。本地实现位于 [`dsh-subprocess-local`](../subprocess-local/README.md)。
|
||||
|
||||
## 约定
|
||||
|
||||
- `spawn(spec)` 立即返回一个活动句柄;`done` 在进程关闭时以退出事实 resolve(`SubprocessOutcome` 不携带输出,也不携带原因分类),仅在 spawn 层面失败时 reject。
|
||||
- spawn 工作目录和可执行文件路径属于提供方的执行世界。`resolveExecutable(command, env?, signal?)` 验证绝对命令,或根据该执行世界清理后的 PATH 加显式覆盖来解析裸名称。
|
||||
- spec 完全显式(argv、cwd、按流划分的 stdio 处置方式(disposition)、宽限期),因为随部署变化的默认值属于调用方的配置,而不属于某个隐藏的子进程服务默认值(`dsh-bash` 的 request/spec 拆分是这条规则的所属模板)。`argv` 绝不经过 shell 解释;需要 shell 的消费方自行传入 `['bash', '-c', command]`。
|
||||
- spec 完全显式(argv、cwd、按流划分的 stdio 处置方式(disposition)、宽限期),因为随部署变化的默认值属于调用方的配置,而不属于某个隐藏的子进程服务默认值(`dsh-shell` 的 request/spec 拆分是这条规则的所属模板)。`argv` 绝不经过 shell 解释;需要 shell 的消费方自行传入 `['bash', '-c', command]`。
|
||||
- stdio 按流采用 Node 风格:`'pipe'` 把原始流交给调用方做自己的协议分帧(LSP 的 JSON-RPC、ACP(Agent Client Protocol)的 ndjson),`'inherit'` 直通父进程描述符以承载诊断输出,收集模式(collect)`{ maxBytes, spill? }` 则缓冲一段有界尾部,外加可选的完整流 spill 文件。收集模式的读取器接受全流字节偏移量且从不消费,因此独立的读取器不会抢走彼此的增量;偏移量滑出内存尾部窗口的读取标记为 `lossy`,并在 spill 文件存在时指向它。收集到的输出在结算后仍可读取。
|
||||
- 终止在每个平台上都以进程树为范围(POSIX 用 detached 进程组并以直接子进程回退;Windows 用 `taskkill /T`):`terminate()`(唯一的终止动词)执行 SIGTERM→宽限期→SIGKILL 升级(幂等,也由 spec 的 abort 信号驱动,进程树消亡后为空操作);`waitForExit(signal?)` 观察整棵进程树的存活状态,使消费方自有的拆卸阶梯能在真正完全停稳后才进入下一层。管理器只响应中止,但绝不判定原因(deadline、拆卸阶梯与原因分类归调用方所有)。
|
||||
- `spawnTerminal(spec)` 是唯一的非管道原语。其句柄负责真实 PTY、UTF-8 文本 I/O、前台进程组检查/信号发送,以及一项须等待的 `terminate()` 操作;该操作会使提供方仍可观察到的每个会话成员完全停稳,并结算在途句柄调用;提供方会记录执行基底特有的可观察性限制。spec 信号只取消分配;句柄一经发布,便负责自身生命周期。顶层进程退出时,输出流在已排队输出之后结束;仍处于活动状态的传输若发生故障,会使 `done` 拒绝。这些操作保留为一项执行基底原语,因为普通管道无法分配控制终端或清理终端会话成员;就绪状态、scrollback 和所有者策略仍归 PTY 消费方所有。
|
||||
|
||||
@@ -67,7 +67,7 @@ export function scrubbedParentEnv(): Record<string, string> {
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
subprocess: SubprocessService
|
||||
subprocess: SubprocessRuntime
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ declare module '@deepseek-ai/cordis' {
|
||||
* in the PTY consumer. Its output stream ends after queued terminal output
|
||||
* when the top-level process exits.
|
||||
*/
|
||||
export abstract class SubprocessService extends Service {
|
||||
export abstract class SubprocessRuntime extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'subprocess')
|
||||
}
|
||||
@@ -139,4 +139,4 @@ export abstract class SubprocessService extends Service {
|
||||
abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>
|
||||
}
|
||||
|
||||
export default SubprocessService
|
||||
export default SubprocessRuntime
|
||||
|
||||
@@ -69,7 +69,7 @@ export interface SubprocessStdio {
|
||||
/**
|
||||
* A fully-specified spawn request. This seam applies no defaults: every
|
||||
* disposition, limit, and directory is explicit, so the caller's own config —
|
||||
* not a hidden subprocess-service default — decides them (the `dsh-bash`
|
||||
* not a hidden subprocess-service default — decides them (the `dsh-shell`
|
||||
* request/spec split is the owning template).
|
||||
*/
|
||||
export interface SubprocessSpawnSpec {
|
||||
@@ -195,7 +195,7 @@ export interface SubprocessHandle {
|
||||
|
||||
/**
|
||||
* Signals supported by the terminal-process primitive. Kept member-identical
|
||||
* to `PtySignal` in `@deepseek-ai/dsh-pty` without a cross-seam dependency;
|
||||
* to `TerminalSignal` in `@deepseek-ai/dsh-terminal` without a cross-seam dependency;
|
||||
* change both together.
|
||||
*/
|
||||
export type SubprocessTerminalSignal = 'SIGINT' | 'SIGTERM' | 'SIGKILL' | 'SIGTSTP' | 'SIGHUP'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { scrubbedParentEnv, SubprocessService } from '@deepseek-ai/dsh-subprocess'
|
||||
import { scrubbedParentEnv, SubprocessRuntime } from '@deepseek-ai/dsh-subprocess'
|
||||
import type {
|
||||
SubprocessHandle,
|
||||
SubprocessOutputRead,
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
* defaulting, shell semantics, and deadlines belong to callers — so this stub
|
||||
* is all an implementation owes the abstract class.
|
||||
*/
|
||||
class StubSubprocessService extends SubprocessService {
|
||||
class StubSubprocessRuntime extends SubprocessRuntime {
|
||||
async resolveExecutable(command: string): Promise<string> {
|
||||
return `/bin/${command}`
|
||||
}
|
||||
@@ -50,10 +50,10 @@ class StubSubprocessService extends SubprocessService {
|
||||
}
|
||||
}
|
||||
|
||||
describe('SubprocessService seam', () => {
|
||||
describe('SubprocessRuntime seam', () => {
|
||||
it('a concrete subclass registers as ctx.subprocess and serves the abstract API', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(StubSubprocessService)
|
||||
await ctx.plugin(StubSubprocessRuntime)
|
||||
const handle = ctx.subprocess.spawn({
|
||||
argv: ['true'],
|
||||
cwd: '/stub',
|
||||
@@ -70,8 +70,8 @@ describe('SubprocessService seam', () => {
|
||||
|
||||
it('loading a second implementation throws (one subprocess service per context — cordis standard)', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(StubSubprocessService)
|
||||
class SecondService extends StubSubprocessService {}
|
||||
await ctx.plugin(StubSubprocessRuntime)
|
||||
class SecondService extends StubSubprocessRuntime {}
|
||||
await expect(ctx.plugin(SecondService)).rejects.toThrow(/service "subprocess" has been registered/)
|
||||
})
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user