revert bash get()/list() removal — keep persistence-only prune

The original prune removed BashExecutor.get()/.list() too, but each is a
one-line accessor over the executor's already-tracked tasks map, and removing
them forced dsh-tool-bash's tests onto a ~35-line onTaskDone completion-tracking
harness just to replace the one-line ctx.bash.get(id) lookup. Per the AGENTS.md
"RFCs are proposals, not golden truth" principle, that disproportionate
migration cost is evidence the methods earn their keep — a test harness IS a
consumer programming against the seam.

Restore get()/list() (seam + LocalBashExecutor impl + the bash tests that used
them, dropping the doneFor/trackCompletions scaffolding). The persistence
has()/delete()/deleteStored removal stands — it had only contract-test callers
and no test-ergonomics cost. The RFC is retitled persistence-only with an
implementation note recording the bash revert.
This commit is contained in:
Tianyi Cui
2026-06-21 06:17:11 +08:00
parent 6ca8c3b99a
commit 24168aee70
12 files changed
+68 -74

No files matched your search

+1
View File
@@ -18,6 +18,7 @@ The split mirrors the LLM seam (`LlmService`/`LlmAdapter`) and the agent-tool su
|---|---|
| `run(spec)` | Foreground execution. Resolves when the command finishes. **Rejects only for infrastructure failures** (unusable workdir, missing shell, pre-aborted signal); nonzero exits, timeout kills, and abort kills resolve with a descriptive `BashRunResult`. |
| `start(spec)` | Background execution. Returns a `BashTask` handle immediately; **no timeout applies** (stop tasks via `kill`). |
| `get(id)` / `list()` | Task lookup. |
| `ownerOf(id)` | The opaque OWNER token recorded for a background task at `start` (from the spec's `owner`), or `undefined` for an unknown id OR a known-but-ownerless task. The executor stores/returns it verbatim and NEVER interprets it — the access POLICY lives in the consumer (`dsh-tool-bash`), which compares `ownerOf(id)` to the caller's token. Storing ownership here (disposed with the executor's fiber) is what makes it survive a consumer HMR reload. |
| `readOutput(id)` | **Incremental** output read — consecutive reads never re-deliver. Reads that lost data to buffer bounds flag `lossy` and point at full-stream spill files. Throws for unknown ids. |
| `kill(id)` | Kill a running task. Returns `false` when it already finished; throws for unknown ids. |
+6
View File
@@ -85,6 +85,9 @@ export abstract class BashExecutor extends Service {
/** Start a background task and return its handle immediately. */
abstract start(spec: BashExecSpec): BashTask
/** Look up a background task by id. */
abstract get(id: string): BashTask | undefined
/**
* The opaque OWNER token recorded for a background task at {@link start}
* (from the {@link BashExecSpec}'s `owner`), or `undefined` for an unknown id
@@ -100,6 +103,9 @@ export abstract class BashExecutor extends Service {
*/
abstract ownerOf(id: string): string | undefined
/** All tracked background tasks (insertion order). */
abstract list(): BashTask[]
/** Read output produced since the previous read. Throws for unknown ids. */
abstract readOutput(id: string): BashTaskRead
+10
View File
@@ -44,10 +44,18 @@ class StubExecutor extends BashExecutor {
return task
}
get(id: string): BashTask | undefined {
return this.tasks.get(id)
}
ownerOf(id: string): string | undefined {
return this.owners.get(id)
}
list(): BashTask[] {
return [...this.tasks.values()]
}
readOutput(id: string): BashTaskRead {
const task = this.tasks.get(id)
if (!task) throw new Error(`unknown bash task "${id}"`)
@@ -80,6 +88,8 @@ describe('BashExecutor service seam', () => {
it('registers as ctx.bash and serves the abstract API', async () => {
const { bash } = await setup()
const task = bash.start(bash.resolve({ command: 'sleep 1' }))
expect(bash.get(task.id)).toBe(task)
expect(bash.list()).toEqual([task])
expect(bash.kill(task.id)).toBe(true)
expect(bash.kill(task.id)).toBe(false)
const result = await bash.run(bash.resolve({ command: 'true' }))