Skip to content

Define the background child run lifecycle contract - #2421

Draft
Aaronontheweb wants to merge 22 commits into
feature/tool-recurrencefrom
feature/background-subagents
Draft

Aaronontheweb wants to merge 22 commits into
feature/tool-recurrencefrom
feature/background-subagents

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

This is the second PR in the stack. Base: #2420.

Child tool calls return durable acceptance receipts. The parent can continue useful work while each child runs. Deferred tools expose recorded status and cancellation. Canonical terminal delivery retains the original requester and task authority.

The first durable consumption of a canonical child result starts a new recurrence window. This permits a report read after earlier reads found no file. Duplicate delivery, status queries, admission, and recovery cannot reset the window. Task identity and receipt failures remain intact. Legacy adoption events retain their prior replay behavior.

Cancellation closes local dispatch before final framework work. A five-second grace period lets the framework write partial evidence. External effects remain unknown. Recovery records child loss without a relaunch.

Why this is safe: The owner commits lifecycle facts before effects. Canonical input and authority checks govern child-result adoption. Durable input identifiers prevent duplicate resets after consumption or compaction. Retained pair checks reject attributed corruption. Transcript text alone cannot authorize a new window.

Validation:

  • The integrated guard candidate passes 177 actor cases and six authority harness cases, with independent review. The authority cases do not execute Stryker mutants.
  • Independent tests reproduce the original false block. The corrected real-file pair passes. Journal-write failure, recovery, sticky receipt failure, and duplicate controls pass.
  • The prompt specifications now match scoped deployment and project guidance. The inherited per-call delta retains the canonical scenario clauses. These document repairs change no runtime prompt bytes.
  • All three active stack changes pass strict OpenSpec validation after the exact combined restack.
  • The historical combined native image at 0da400bb passes one real-model held-child trial and one nonadjacent-cycle recovery trial, with independent review.
  • Those model trials do not exercise the two-Missing-read counterexample. One fresh scripted live regression at combined head f6685d5f now passes both the canonical verifier and a separate read-window predicate. Independent review checks actual provider history, artifact bytes, and continued guard protection.
  • The earlier scripted V2 result remains false. Its added predicate misclassifies canonical context nudges; the corrected predicate retains extra-task and changed-prompt rejection.
  • Later coding and plan model trials at f48e9fbe remain false. The coding trial exceeds its observer window and violates exact command limits. The plan trial completes both children and delivers the plan, but alters its source identity and table format.
  • The rollback runbook describes a backup, compatible reader, and restoration procedure. Its checks prove preparation, not an executed operational rollback.

PR2 head: 255c8334. All 29 hosted checks pass for this exact head. The former combined head f6685d5f also passes all 29 checks, including 29 native tapes and 10 scenarios on each native platform. These results do not establish acceptance for a later combined head.

The PR remains a draft. Model acceptance and final verification remain open. Merge and rollout require separate decisions.

Comment thread evals/child_run_evals.py
from pathlib import Path, PurePosixPath
import re
import subprocess
import threading
var notice = Assert.IsType<CompactionOutput>(await subscriber.FishForMessageAsync<object>(
message => message is CompactionOutput, Ceiling, cancellationToken: TestContext.Current.CancellationToken));
Assert.True(notice.Summarized);
Assert.True(notice.MessagesAfter < notice.MessagesBefore);
"The fixture acknowledged a foreign trial.");
var binding = snapshot.GetProperty("binding");
Require(binding.GetProperty("accepted").GetProperty("run_id").GetString() == Acceptance!.RunId
&& binding.GetProperty("accepted").GetProperty("scope_id").GetString() == Acceptance.ScopeId
_armed = false;
captured = Held.Task.IsCompletedSuccessfully ? Held.Task.Result : null;
}
captured?.Receiver.Tell(captured.Envelope.Message, captured.Envelope.Sender);
await subscriber.FishForMessageAsync<object>(message => message is TurnCompleted, Ceiling,
cancellationToken: TestContext.Current.CancellationToken);
Assert.Contains(_main.ReceivedMessages[^1].SelectMany(message => message.Contents.OfType<FunctionResultContent>()),
result => result.Result?.ToString()?.Contains(findingText, StringComparison.Ordinal) == true);
Assert.Equal("Accepted", body.RootElement.GetProperty("state").GetString());
Assert.Equal(accepted.Run.RunId.Value, body.RootElement.GetProperty("run_id").GetString());
Assert.Equal("check_agent_run", body.RootElement.GetProperty("control_tool").GetString());
Assert.DoesNotContain(_main.ReceivedMessages[^1], message => message.Text?.Contains("child final", StringComparison.Ordinal) == true);
Comment thread src/Netclaw.Actors.Tests/Sessions/SubAgentSpawnIntegrationTests.cs Fixed
@Aaronontheweb
Aaronontheweb force-pushed the feature/background-subagents branch from 66b1f14 to 2da97ac Compare October 9, 2026 23:28
var callbackEntered = NewSignal();
var callbackExited = NewSignal();
using var callbackRelease = new ManualResetEventSlim();
var provider = new UncooperativeChildClient(_child)
evidenceSource = "persistent DaemonClient output; not the headless CLI envelope"
}));
else
Console.Write(transcript.ToString());
@Aaronontheweb
Aaronontheweb force-pushed the feature/background-subagents branch from 4db39fe to 832ebff Compare October 10, 2026 01:52
Comment on lines +162 to +168
foreach (var id in new[] { "window-probe-1", "window-probe-2" })
{
var actual = Assert.Single(after.OfType<ToolCallRecorded>(),
evt => evt.ToolResult.ToolCallId == new ToolCallId(id));
Assert.False(actual.LoopObservation!.Synthetic);
Assert.True(Array.IndexOf(after, childAdoption) < Array.IndexOf(after, actual));
}
Comment on lines +147 to +155
foreach (var id in new[] { "report-after-3", "report-after-4" })
{
var read = Assert.Single(events.OfType<ToolCallRecorded>(),
item => item.ToolResult.ToolCallId == new ToolCallId(id));
Assert.True(Position(adoption) < Position(read));
Assert.False(read.LoopObservation!.Synthetic);
Assert.Equal((int)ToolInvocationOutcomeCategory.Success, read.LoopObservation.Category);
Assert.Equal(report, read.ToolResult.Content);
}
@Aaronontheweb
Aaronontheweb force-pushed the feature/background-subagents branch 2 times, most recently from 7ffc60f to 8d69f2b Compare October 11, 2026 16:14
…ill API

dev renamed ToolOutputSpillLocation.TryEnsureSessionDirectory to
EnsureSessionWorkspaceDirectory, which now returns an error string instead of
a bool. Update the cancelled-child report writer to match.
…rminal

The child's PostStop death path emitted SubAgentResult (the inline-path
message), but a background child's owner only handles BackgroundChildTerminal.
Route the ActorStopped result through BackgroundChildTerminal with the run and
scope identities when the child runs in the background, so the owner records
the terminal instead of dead-lettering it.
@Aaronontheweb
Aaronontheweb force-pushed the feature/background-subagents branch from 8d5aa38 to 2494a01 Compare October 12, 2026 02:20
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