Skip to content

Tasks: a task tree is bounded in depth, count and spend, and a worker does its own task - #2615

Merged
AbirAbbas merged 13 commits into
devfrom
fix/tasks-tree-bounds
Nov 13, 2026
Merged

AbirAbbas merged 13 commits into
devfrom
fix/tasks-tree-bounds

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Four Tasks fixes merged only into workstreams/main (#2446, #2452, #2461, #2464). Its integration pull request, #2306, was closed unmerged after app/v3 was squashed onto dev (#2561), so none of them reached dev. This carries them, one commit each, in their original order.

What changes for a person:

The original pull requests hold the full validation contracts.

dev still has each fault

Before each commit, I ran that fix's own tests against the tree without its code change. Where a test needed a symbol the fix adds, I ran the tests that compile without it.

Fix Tests run before the code change Result
#2446 TestRunWorkerRefusesDuplicateTitles, TestRunWorkerTreeBoundsRefuseWholeBatches (on dev 80b5861, with only the two limit constants supplied) fail: duplicate was accepted in all six cases; bound was not explained: exit 0 for count, batch and depth
#2452 TestStopRootCancelsOpenTasksEvenWhenRootAlreadyEnded, TestStoppedRunRejectsALateRootWorkersReview, TestDrainedWorkerReleasesItsClaimWithoutDiscardingRetainedWork, TestRetainedReadyWorkReadsQueuedOnlyAfterItsRunLeaves all four fail, e.g. stop left open work, late return reopened or rewrote stopped work
#2461 TestCrewFactoryGivesEachCheckItsOwnCeiling, TestCrewFactorySeparatesCheckEstimatesWithASharedDay, TestScopedCheckSpendEndsTheWholeRunAtTheTaskCap all three fail: check-b call 0: the check stopped at its spend ceiling of $0.12
#2464 TestNodeTree* (without the one test that reads the new checkpoint field) 8 of 10 fail, e.g. duplicate refusal missing "no: this run already has a task named", queued node bought a request

Nothing was dropped: all four are carried whole.

What differs from the originals

Tests

CI on this head (c7721f5) is green: light gate, touched codeaf, touched session, touched tui3, touched rest, windows native tests and the required check.

Locally on macOS, make pr-ready did not pass as one command. The PR is marked ready on the strength of CI and the comparison with dev below:

Step Result
make test-quick pass
make test-packed-manual manual-gates pass
touched ./internal/plandb pass
touched ./internal/run TestDelegateForwarderReachesNote201 fails; the runner reports already failing on the base
touched ./internal/session (8 shards) 5 failures, each reported already failing on the base: TestACommandLookFailsOnAFailingPipeStage, TestWatchTickFailsOnAFailingPipeStage, TestBashBeltTruncationCapFollowsTheWindow, TestBashBeltTruncationWritesHeadTailAndPath, TestTheFixedPrefixStaysUnderItsBudget
touched ./internal/tui3 (8 shards) pass: Every selected test passed on its first run.
touched ./cmd/codeaf (8 shards) red, twice: 30 and 28 failing tests, over the runner's cap of 5, so it made no retries or base probes

The cmd/codeaf reds are 21 distinct tests across the two runs, in Factory, memory pause, hosted take-back, doctor and shared tools; none is a task test. Run by name:

  • 12 also fail on unmodified dev (ffeeb78) on this Mac: TestAppFactoryPlanFilesAnswer, TestDoReturnsAfterCancelledAccountingAndItsWriterStop, TestFactoryCommandStepRetryAfterOutOfAttempts, TestFactoryMeasuredOnlyPublication, TestFactoryPersonStopInFlightWakeAndPersonChat, TestFactoryRetryActionsDoNotReportStopped, TestFactoryStepRetryWaitsForPreviousLeaseCleanup, TestFactoryWriteUsesJobBaseAcrossAttempts, TestMemoryPauseResumeAfterTrailingProgress, TestMemoryPauseRetainsPendingForResume, TestMemoryPausedJobLifecycle, TestProcessCloseJoinsTheUsageWriter.
  • The other 9 pass on this branch when run by name (ok ... 48.649s). They failed only inside the full runs, which shared the machine with other test suites (load average 26 to 39 during the first run).

Each fix's own tests pass on this branch, along with the neighbouring spend-guard, crew, division and quick-task tests.

Changes after review

A pre-merge review and a hand test found defects that the carried commits brought along. They are fixed in six new commits (2ffcc3e to c245451, head now c245451), and two independent verifiers checked the fixes. Each test below failed on the previous head (c7721f5) and passes now.

  • A per-task raise did not raise the tree's dollar ceiling.
    • Found: on the run road, the tree ceiling read only the configured crew.task_cap. Raising one task's limit through a proposal, a reroute or a live raise in /crew changed nothing, so a task raised to $8 still stopped at $5. On a live raise, the guard's refusal also still named the old figure.
    • Changed: the ceiling is now the highest of the configured cap, the task's own raised cap and the crew guard's cap. The engine reads it while the run goes (Limits.CostCeiling) and the ceiling only ever rises. SpendGuard.raiseTaskCap updates the figure and the refusal words together. A live raise also lifts the running tree.
    • Tests: TestARaisedProposalRunsUnderItsRaisedTreeCeiling and TestALiveRaiseLiftsTheRunningTreeCeiling (internal/session); TestARaisedCostCeilingIsReadWhileTheRunGoes (internal/run).
  • On the node road (CODEAF_TASK_BELT=node), the tree total counted a finished child's spend twice.
    • Found: when a child finishes, its bill is added to its own node and also to the worker that proposed it. The tree total added both, so a child counted twice and a grandchild three times, and the tree hit its limit early.
    • Changed: the fold now marks the dollars it moved. The tree total counts each node's bill minus what was folded into it, so each dollar counts once. The checkpoint gains foldedUsd, so a restored tree also counts each dollar once.
    • Test: TestNodeTreeSpendCountsAPartFoldedIntoItsOwnerOnce (real nest harness, real folds at two levels).
  • The per-task limit sentence was appended at the end of the report and went missing from the screen.
    • Found: "this task reached its $N limit · raise it in /crew" was appended as an extra line instead of leading the report. When the guard refused the root it appeared twice. In the hand test, the engine's new tree ceiling ended the run before the guard refused anything, so the routed crew recorded no stop, and the line under the task row and the entry in the Tasks column did not show.
    • Changed: when the per-task limit ends a run, that sentence becomes the run's failure text. It leads the report and the outcome note exactly once. A routed crew records it as its stop, which brings back the line under the row and the Tasks-column entry. No TUI code changed.
    • Tests: TestAPerTaskLimitEndingSaysItsSentenceOnceWhereThePersonLooks (internal/session, through StartTask to settle); TestAPerTaskLimitEndingIsSaidOnceUnderItsRowAndListed (internal/tui3).
  • The tasks manual said a proposed task reads the person's message.
    • Found: "What a task is" in internal/manual/chat/tasks.md said every brief carries the person's message and a journal pointer. A task proposed on the run road actually reads only its own assignment.
    • Changed: the paragraph now separates a typed task from a run-road proposal and points to "What a proposed task reads".
    • Test: TestTasksManualSaysAProposedRunReadsOnlyItsAssignment (internal/manual); make test-packed-manual manual-gates passes.
  • plandb task pivot counted the tasks it was replacing. This finding was marked "evaluate" in review. It was real and the fix was small, so it is fixed here.
    • Found: a pivot adds the new tasks before it cancels the old ones. The add has to come first, because cancelling the last open child completes the parent. So the five-per-worker limit and the duplicate-title check counted the tasks about to be cancelled, and a worker with three parts could not pivot to three new ones.
    • Changed: the bounded add now takes the list of tasks being replaced and leaves out the ones still open when it counts totals, the worker's allowance and taken titles. A replaced title can be reused. Finished work the pivot keeps still counts.
    • Test: TestWorkerPivotCountsOnlyTheWorkThatSurvivesIt (internal/plandb). A six-part pivot is still refused whole.

Docs moved with the behaviour:

  • internal/manual/chat/tasks.md, how-tasks-run.md and models-and-cost.md, plus a new manual test.
  • The change entries 2446, 2452 and 2464.
  • This PR's entry 2615-tasks-fixes-carried.md, which now lists these fixes.

The wire protocol and the system prompt did not change. The new RunSpec.CostCeiling and Limits.CostCeiling are internal Go fields.

Earlier sections that are now out of date:

  • The conflict note under "What differs from the originals" no longer holds. The per-task limit words are no longer appended to noticeResultWords' report. They lead it as the run's failure text.
  • The CI results under "Tests" are for the old head, c7721f5.

Left as is: TestAClosedNodesDollarsAreStampedOnTheConversation fails on this Mac the same way on the new head, on c7721f5 and on unmodified dev (ffeeb78): meta.json says the conversation spent 0 once its node closed, want the node's $0.75. This PR does not cause it, so it is not changed here.

  • A finished run is not a limit stop (found by the review of the fixes above). The engine ends a run done with Limit cost when its final receipt crosses the tree's ceiling; the limit helpers fired on Limit cost alone, so such a finished task got the per-task limit as its crew's stop: a limit warning on the task sheet, no "run again with a stronger crew", and no completion announced. taskLimitWords now answers nothing for a run the engine ended done, as dev never stopped a crew for one. Proving test: TestAFinishedRunAtItsCeilingIsNoPerTaskLimitStop (failed with the crew stopped on "this task reached its $5 limit · raise it in /crew" before the change, passes after). Commit bd15299.

🤖 Generated with Claude Code

AbirAbbas and others added 6 commits November 12, 2026 12:13
…h, count and spend

Carries #2446 (a4b3079 on workstreams/main) onto dev. The one conflict, in
beltRunNotice, keeps dev's noticeResultWords and adds the per-task limit words
to its report.

Co-Authored-By: Claude Mythos 5.2 <[email protected]>
…er their own task, run subtasks keep their parent

Carries #2452 (331433e on workstreams/main) onto dev. It applies cleanly on
top of the carried #2446.

Co-Authored-By: Claude Mythos 5.2 <[email protected]>
Carries #2461 (c5c8ea1 on workstreams/main) onto dev. It applies cleanly on
top of the carried #2446 and #2452.

Co-Authored-By: Claude Mythos 5.2 <[email protected]>
…end total

Carries #2464 (e30414b on workstreams/main) onto dev. It applies cleanly on
top of the carried #2446, #2452 and #2461.

Co-Authored-By: Claude Mythos 5.2 <[email protected]>
…cap question reaches tasks

The carried text made the composer-cap section 3,300 characters and the
question "how much money can a task spend before it stops and asks me" stopped
reaching the tasks page. The tree limit now has its own headed section.

Co-Authored-By: Claude Mythos 5.2 <[email protected]>
AbirAbbas and others added 7 commits November 12, 2026 19:02
A part a worker proposes runs on that worker and folds its bill into it
when it lands, so the parent's spend already holds the part while the
part's own row keeps its bill too. The tree total summed every node's
spend, counting a child twice and a grandchild three times, and trees on
CODEAF_TASK_BELT=node stopped at a fraction of their limit.

The fold now marks the dollars it moved into the ledger worker, the mark
travels with the worker's money into its node, and the tree total takes
those folded dollars off each node's bill. The checkpoint keeps the
folded part so a restored tree counts the same way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The whole-tree dollar ceiling on the run road read the profile's per-task
cap alone. A raise the person gave one task (a proposal's or a rerun's
raise_cap_usd, routed into the crew's guard, or a running task's live
change) reached only the guard, so the engine still stopped the run at
the old figure and its ending told the person to raise a limit they had
just raised.

The tree ceiling now starts from the higher of the profile's cap, the
hand-off's raise and the crew guard's cap. The run spec carries a live
ceiling the engine reads at every spend reading, and a live raise lifts
it along with the guard. The guard's refusal sentence names the raised
figure, and its per-task limit is read under its lock now that a raise
can move it while calls are made.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The per-task limit sentence was appended to whatever the run's ending
already said. When the engine's tree ceiling cut a run before any seat's
call was refused (the hand test: /crew cap task 0.002, then a /task),
the note led with "ran and did not finish" and kept the sentence on a
second line, and nothing recorded the crew's stop, so the row had no
line under it and the Tasks column did not list the task. When the
crew's guard refused the root's own call, the sentence was the worker's
failure and was appended again, so the report said it twice.

The ending now puts the sentence where a worker's failure stands, once,
in the report and the outcome note, and a routed crew records it as its
stop when nothing else did, which is what draws it under the row and
lists the task as needing the person, as before the tree ceiling.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The tasks page's "What a task is" said every task's brief carries the
person's message word for word and a journal pointer, with a run-road
proposal only putting its own work first. A proposed run's brief is its
title, work, deliverable and done-condition and nothing of the
conversation (proposalBrief), as how-tasks-run already says. The
paragraph now tells the two apart, and a manual test holds it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`plandb task pivot` adds its replacement children before it cancels the
subtree they replace, because cancelling a parent's last open child
completes the parent and the add would then be refused. The new tree and
worker gates read the doomed children as still open, so a worker that
had added three parts and replaced them with three others was refused at
its five, and a replacement keeping a replaced part's title was refused
as a duplicate.

The bounded add now takes the open tasks the pivot is about to cancel
and leaves them out of the counts and the taken titles. Finished work
the pivot keeps still counts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The engine ends a run done with Limit cost when its final receipt crosses
the tree's ceiling. The limit helpers fired on Limit cost alone, so such a
finished run recorded the per-task limit as its crew's stop: the task sheet
drew a limit warning, "run again with a stronger crew" was hidden and the
routine completion was dropped. taskLimitWords now answers nothing for a
run the engine ended done, as dev never stopped a crew for one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@AbirAbbas
AbirAbbas merged commit 9c88bc0 into dev Nov 13, 2026
15 of 19 checks passed
@AbirAbbas
AbirAbbas deleted the fix/tasks-tree-bounds branch November 13, 2026 01:17
AbirAbbas added a commit that referenced this pull request Nov 13, 2026
Brings in #2613, #2612, #2614, #2618 and #2615. #2618 changes the
visibility check this branch's job-events cursor reads: a spent GitHub
rate limit returns GitHub's retry time instead of a cached denial, and is
remembered per token until then.

The two meet in repoAllowed, which this branch made answer (allow, known).
A remembered limit answers false, false: a limit says nothing about the
repository, so the job-events page ends before it as before any undecided
one, as a fresh limit already does (askRepo's cache=false).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.

1 participant