Skip to content

🤖 feat: return task_await on first completion via min_completed#3414

Merged
ammario merged 3 commits into
mainfrom
mux-agent-y1qe
May 28, 2026
Merged

🤖 feat: return task_await on first completion via min_completed#3414
ammario merged 3 commits into
mainfrom
mux-agent-y1qe

Conversation

@ammar-agent

Copy link
Copy Markdown
Collaborator

Summary

task_await now accepts an optional min_completed integer (default 1). It returns as soon as that many awaited tasks have completed — by default the first completion — instead of always blocking until every awaited task finishes. The parent can act on each result as it lands (e.g. integrate variant lane #45 while #32/#69 keep running) and re-await the remainder, rather than idling until the whole batch is done.

Background

When the parent spawns a grouped batch (task with n for best-of-N or variants), there was often useful dependent work available after just one child completed — most obviously for variants, where each lane is independent. But both the foreground task path and task_await used Promise.all and only returned once the slowest task settled, and the tool/prelude guidance framed the flow as "launch → await as a batch → synthesize all." Nothing let the parent begin work after the first child.

Implementation

  • Schema (toolDefinitions.ts): new min_completed: z.number().int().min(1).nullish() on TaskAwaitToolArgsSchema; absent/null1.
  • Handler (task_await.ts): the per-task await body is now awaitOne(taskId, taskSignal). Each task gets its own AbortController chained to the tool-call signal. A small coordinator resolves once min_completed tasks have completed or every task has otherwise settled (so an unreachable threshold still returns promptly). Still-pending "losers" are then aborted to detach their waiters — which only removes the in-memory waiter / interrupts a bash poll; the child keeps running and its report stays cached and re-awaitable on a later call. Losers are reported with a live running/queued status snapshot rather than an error (a real tool-call interrupt is still distinguished and surfaced as error: "Interrupted").
  • timeout_secs: 0 stays a non-blocking snapshot of every task regardless of min_completed.
  • Clamped to the number of awaited tasks (over-large values behave like "wait for all").
  • Guidance: task/task_await descriptions and the <best-of-n> / <task-variants> prelude blocks now steer independent lanes toward the first-completion loop, and tell best-of-N synthesis (which must compare every candidate) to pass min_completed equal to the batch size or use a foreground grouped spawn. Auto-generated docs (system-prompt.mdx, hooks/tools.mdx) and the built-in skill snapshot were regenerated via make fmt.

The foreground task (run_in_background: false) path is intentionally unchanged — grouped spawns still return all reports, which remains the natural "give me every candidate" path.

Validation

  • New task_await tests: default returns after first (rest running); min_completed = total waits for all; min_completed = k returns after the k-th; clamp; re-awaitable loser resolves on a follow-up call; unreachable threshold returns promptly; timeout_secs:0 non-blocking with min_completed. The first-completion test also asserts the loser's per-task signal is aborted while the winner's is not.
  • make static-check passes (typecheck, eslint, formatting, docs-sync).

Risks

min_completed defaults to 1, so this is a behavior change for task_await: an existing background-spawn-then-await flow that expected all reports now returns after the first completion. Mitigations: the all-settled fallback + clamp keep results well-formed, losers remain re-awaitable (no work lost, children not terminated), and guidance steers aggregation flows to pass the batch size. Affected area is limited to sub-agent orchestration.


Generated with mux • Model: anthropic:claude-opus-4-8 • Thinking: xhigh • Cost: $5.76

task_await now returns once min_completed awaited tasks have completed
(default 1 = first completion) instead of always blocking on every task.
The parent can act on each result as it lands while the rest keep running,
then re-await the remainder. Losing waits are detached via per-task
AbortControllers without terminating the children, which stay re-awaitable.

Updated task/task_await tool descriptions and the best-of-n/task-variants
prelude to steer aggregation flows to pass the batch size.
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8ab1d87f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/tools/task_await.ts
@mintlify

mintlify Bot commented May 28, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Mux 🟢 Ready View Preview May 28, 2026, 8:37 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

task_await's early-return aborts still-pending bash getOutput reads once
min_completed is satisfied. getOutput's interrupted path advanced
outputBytesRead without flushing accumulatedRaw, so an aborted read could
drop an unread line fragment and the next read would resume past it. Save
the consumed bytes back into incompleteLineBuffer before returning. Adds a
deterministic regression test.
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed the P1 on interrupted bash reads. BackgroundProcessManager.getOutput now flushes the bytes consumed this call (previousBuffer + accumulatedRaw) back into incompleteLineBuffer before taking the interrupted return path, so aborting a still-pending read (including task_await's early return once min_completed is satisfied) no longer drops an unread line fragment — the next read resumes from exactly where it left off. Added a deterministic regression test in backgroundProcessManager.test.ts that appends a newline-less fragment, aborts the read, then asserts the completed line is preserved on the next read.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a5f038601

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/tools/task_await.ts
If an awaitOne path rejected outside its internal catches (e.g. a bash
getProcess/getOutput read throwing), the recording callback never inserted a
result, so the min_completed gate could never reach the threshold or
all-settled and would stall. Wrap each per-task promise so a stray rejection
becomes a recorded error result; the task counts as settled and the call
returns promptly. Adds a regression test.
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed the P2 on rejected waiters. Each per-task promise is now wrapped so a stray rejection escaping awaitOne's internal catches (e.g. a bash getProcess/getOutput read throwing) is converted into a recorded { status: "error", taskId, error } result. The task therefore counts as settled, so the min_completed gate can always reach either the threshold or the "all settled" fallback and return promptly with the error surfaced — no stall. Added a regression test (surfaces a waiter that rejects outside its internal catches without stalling).

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ammario ammario merged commit 49326f8 into main May 28, 2026
23 checks passed
@ammario ammario deleted the mux-agent-y1qe branch May 28, 2026 22:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants