fix review findings: stale seam docs + race-free doneFor test helper

Codex review of the PR2 diff caught doc/comment sites doc-sync does not gate
(core-data-structures prose) and a latent test-helper race:
- docs/core-data-structures/persistence.md + bash.md, sqlite README, and two
  source comments (coordinator.ts, jsonl.spec.ts) still listed the removed
  has/delete/get/list methods — updated to the surviving four-method
  persistence surface and the get/list-free bash seam.
- doneFor(ctx, id) attached its onTaskDone listener lazily, after the task
  could already have closed (e.g. `true`), so it could miss the completion and
  hang. Replaced with trackCompletions(ctx): one eagerly-installed listener
  (mounted in setup() before any task starts) records every completion, and
  doneFor resolves immediately for an already-finished task or on completion
  otherwise. Race-free, and there is no get-by-id seam left to poll instead.
This commit is contained in:
Tianyi Cui
2026-06-21 02:45:21 +08:00
parent 7792347c4f
commit 5f9d10c587
6 changed files with 41 additions and 19 deletions

View File

@@ -24,6 +24,7 @@ async function setup() {
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
;(ctx.bash as LocalBashExecutor).internals = { spillDir, graceMs: 200 }
await ctx.plugin(ToolBash)
trackCompletions(ctx)
return ctx
}
@@ -67,22 +68,42 @@ function text(result: { content: { type: string; text?: string }[] }): string {
}
/**
* Resolve once the background task with `id` completes. The task is started
* indirectly (via `ctx.tools.execute`), so `start()`'s return is not accessible
* here; the executor's `onTaskDone` listener delivers the SAME task object on
* completion, which is the surviving seam for awaiting a task by id.
* Per-context background-completion tracker. The task is started indirectly
* (via `ctx.tools.execute`), so `start()`'s return is not accessible here, and
* there is no get-by-id seam to poll current state — the only surviving way to
* await a task by id is the executor's `onTaskDone` listener. Registering that
* listener lazily (after the task may have already closed) would miss the
* completion and hang; so {@link trackCompletions} installs ONE listener
* EAGERLY (before any task starts) that records every completion, and
* {@link doneFor} resolves from that record — immediately if the task already
* finished, otherwise when it does. Call `trackCompletions(ctx)` right after
* the executor is mounted (`setup()` does this for you).
*/
function doneFor(ctx: Context, id: string): Promise<BashTask> {
return new Promise<BashTask>((resolve) => {
const dispose = ctx.bash.onTaskDone((task) => {
if (task.id === id) {
dispose()
resolve(task)
}
})
const completions = new WeakMap<Context, { done: Map<string, BashTask>; waiters: Map<string, (task: BashTask) => void> }>()
function trackCompletions(ctx: Context): void {
const state = { done: new Map<string, BashTask>(), waiters: new Map<string, (task: BashTask) => void>() }
completions.set(ctx, state)
ctx.bash.onTaskDone((task) => {
const waiter = state.waiters.get(task.id)
if (waiter) {
state.waiters.delete(task.id)
waiter(task)
} else {
state.done.set(task.id, task)
}
})
}
/** Resolve (with the task object) once the background task `id` has completed. */
function doneFor(ctx: Context, id: string): Promise<BashTask> {
const state = completions.get(ctx)
if (!state) throw new Error('trackCompletions(ctx) must be called before doneFor(ctx, …)')
const already = state.done.get(id)
if (already) return Promise.resolve(already)
return new Promise<BashTask>(resolve => state.waiters.set(id, resolve))
}
class LossyReadBashExecutor extends BashExecutor {
private readonly task: BashTask = {
id: 'bash-lossy',
@@ -312,6 +333,7 @@ describe('background tools', () => {
await ctx.plugin(LocalBashExecutor, { maxOutputBytes: 100 })
;(ctx.bash as LocalBashExecutor).internals = { spillDir, graceMs: 200 }
await ctx.plugin(ToolBash)
trackCompletions(ctx)
const started = await call(ctx, 'bash', { command: 'for i in $(seq 1 200); do printf "line-%04d\\n" $i; done', description: 'test command', run_in_background: true })
const id = /task (bash-\d+)/.exec(text(started))![1]!