Repository navigation
feat(tasks): background concurrency cap + graceful kill, DAG cut (M3.… - #23
Merged
Merged
Conversation
…6 / §3)
The final v3.1 item: resource control for background tasks. Two mechanisms,
the task-dependency DAG explicitly NOT built (Claude Code has no DAG scheduler;
it would be a parallel workflow engine against the repo's "extend primitives,
no control plane" spirit — see docs/v3-kernel-roadmap.md §3 M3.6).
Concurrency cap — createTaskScheduler({ store, maxConcurrent }) is a FIFO
admission semaphore: tasks beyond the cap stay "pending" (queued) until a
running one reaches a terminal state and frees a slot. maxConcurrent 0 (default)
= unlimited, so this is non-breaking and opt-in. The run+terminal-state logic is
extracted into a shared runManagedTaskBody used by both startManagedTask
(unbounded, unchanged) and the scheduler. The cap is a strong guarantee only for
in-process managed tasks the scheduler admits; detached out-of-process workers
are not gated across processes (known v1 limit, documented).
Graceful kill — markTaskKilled now terminates gracefully: in-process tasks
cancel cooperatively via their AbortSignal (no signal sent), and a detached
worker's pid gets SIGTERM -> graceMs grace -> SIGKILL only if still alive. The
signal sender is injectable (signalProcess) so the escalation sequence is
asserted offline without real processes. Windows has no real SIGTERM
(process.kill -> TerminateProcess) so the first signal already terminates and
the escalation is a documented no-op.
- core: createTaskScheduler + TaskScheduler, runManagedTaskBody refactor,
graceful markTaskKilled (KillOptions/ProcessSignaler), ToolContext.taskScheduler.
- tools: runBackgroundSubAgent routes through context.taskScheduler when present,
else startManagedTask (unchanged).
- cli: MYAGENT_MAX_BACKGROUND_TASKS env builds the scheduler in the agent path.
- tests: 6 new — cap+queue (deterministic via started barriers), queue-cancel,
unbounded default, SIGTERM->SIGKILL escalation, SIGTERM honored (no escalation),
in-process pid not signaled.
- docs: CLAUDE.md background-tasks + env; roadmap M3.6 marked delivered.
220 tests pass (+6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…6 / §3)
The final v3.1 item: resource control for background tasks. Two mechanisms, the task-dependency DAG explicitly NOT built (Claude Code has no DAG scheduler; it would be a parallel workflow engine against the repo's "extend primitives, no control plane" spirit — see docs/v3-kernel-roadmap.md §3 M3.6).
Concurrency cap — createTaskScheduler({ store, maxConcurrent }) is a FIFO admission semaphore: tasks beyond the cap stay "pending" (queued) until a running one reaches a terminal state and frees a slot. maxConcurrent 0 (default) = unlimited, so this is non-breaking and opt-in. The run+terminal-state logic is extracted into a shared runManagedTaskBody used by both startManagedTask (unbounded, unchanged) and the scheduler. The cap is a strong guarantee only for in-process managed tasks the scheduler admits; detached out-of-process workers are not gated across processes (known v1 limit, documented).
Graceful kill — markTaskKilled now terminates gracefully: in-process tasks cancel cooperatively via their AbortSignal (no signal sent), and a detached worker's pid gets SIGTERM -> graceMs grace -> SIGKILL only if still alive. The signal sender is injectable (signalProcess) so the escalation sequence is asserted offline without real processes. Windows has no real SIGTERM (process.kill -> TerminateProcess) so the first signal already terminates and the escalation is a documented no-op.
220 tests pass (+6).