Repository navigation
Replace tool iteration limits with exact recurrence guards - #2420
Draft
Aaronontheweb wants to merge 45 commits into
Draft
Aaronontheweb wants to merge 45 commits into
Aaronontheweb wants to merge 45 commits into
Conversation
| Assert.Equal(1, _client.CompactionCount); | ||
| var compaction = Assert.Single(_compactions); | ||
| Assert.True(compaction.Summarized); | ||
| Assert.True(compaction.MessagesAfter < compaction.MessagesBefore); |
| var evt = new ToolTaskAdopted | ||
| { | ||
| SessionId = _sessionId, TurnContext = admitted.TurnContext, InputIds = inputIds.ToArray(), AdoptedAtMs = NowMs(), | ||
| ContinuedJobKey = admitted.SourceBackgroundJobId?.Value |
| { | ||
| _state = _state.Apply(closed); | ||
| _inFlightDedup.CompleteBackgroundJob(input.SourceBackgroundJobId); | ||
| _log.Error("Background job lineage contract failure job={JobKey}", input.SourceBackgroundJobId?.Value); |
| if (authority.SessionId != evt.SessionId || evt.LoopAdmission?.TaskId != authority.TurnId.Value | ||
| || evt.LegacyTaskContext is null || !SessionState.SameCanonicalContext(authority.ToRecord(), evt.LegacyTaskContext)) | ||
| throw new InvalidDataException("A legacy tool baseline differs from the original approval task."); | ||
| var firstCall = evt.LoopAdmission.Calls.FirstOrDefault() |
| if (cmd.Source?.BackgroundJobId is not null | ||
| && (cmd.Source.Principal != PrincipalClassification.VerifiedAutomation | ||
| || cmd.Source.Provenance.TransportAuthenticity != TransportAuthenticity.LocalProcess | ||
| || cmd.Source.Provenance.SourceKind?.Value != BackgroundJobManagerActor.SourceKind)) |
Aaronontheweb
added this pull request to stack #2423
October 9, 2026 01:13
| { | ||
| context?.AddModelInputFile(imagePath, "diagram.png", "image/png"); | ||
| ArgumentNullException.ThrowIfNull(context); | ||
| context.AddModelInputFile(imagePath, "diagram.png", "image/png"); |
| contexts.Add(adopted.TurnContext); | ||
| eventOrder.Add($"adopt:{adopted.TurnContext.TurnId}"); | ||
| }); | ||
| Recover<ToolCallRecorded>(result => eventOrder.Add($"result:{result.ToolResult.ToolCallId?.Value}")); |
| IReadOnlyList<SerializableChatMessage> history, | ||
| SerializableChatMessage assistantMessage, | ||
| string callId) | ||
| => FindToolResultsFor(history, assistantMessage).Any(message => message.ToolCallId?.Value == callId); |
Comment on lines
+242
to
+307
| catch (Exception failure) | ||
| { | ||
| var failures = new List<Exception> | ||
| { | ||
| operationDeadline.IsCancellationRequested && !cancellationToken.IsCancellationRequested | ||
| ? new TimeoutException("The process exceeded the ten-second operation deadline.", failure) | ||
| : failure | ||
| }; | ||
| if (process.StartInfo.RedirectStandardInput) | ||
| { | ||
| try | ||
| { | ||
| // EOF lets the Git wrapper wait for its child before the wrapper exits. | ||
| process.StandardInput.Close(); | ||
| if (!operationDeadline.IsCancellationRequested) | ||
| await process.WaitForExitAsync(operationDeadline.Token); | ||
| } | ||
| catch (Exception inputTeardownFailure) | ||
| { | ||
| failures.Add(inputTeardownFailure); | ||
| } | ||
| } | ||
| try | ||
| { | ||
| if (!process.HasExited) | ||
| { | ||
| try | ||
| { | ||
| process.Kill(entireProcessTree: true); | ||
| } | ||
| catch (InvalidOperationException) when (process.HasExited) | ||
| { | ||
| // The process can exit between the ownership check and termination. | ||
| await process.WaitForExitAsync(CancellationToken.None); | ||
| } | ||
| } | ||
| await process.WaitForExitAsync(CancellationToken.None); | ||
| } | ||
| catch (Exception teardownFailure) | ||
| { | ||
| failures.Add(teardownFailure); | ||
| } | ||
|
|
||
| // Stop pipe reads after teardown. Their errors must not replace the operation failure. | ||
| try | ||
| { | ||
| await deadline.CancelAsync(); | ||
| } | ||
| catch (Exception cancellationFailure) | ||
| { | ||
| failures.Add(cancellationFailure); | ||
| } | ||
| try | ||
| { | ||
| await drains; | ||
| } | ||
| catch (Exception drainFailure) | ||
| { | ||
| failures.Add(drains.Exception is { } drainErrors ? drainErrors : drainFailure); | ||
| } | ||
| var output = standardOutput.IsCompletedSuccessfully ? await standardOutput : "<output did not complete>"; | ||
| var error = standardError.IsCompletedSuccessfully ? await standardError : "<error output did not complete>"; | ||
| throw new AggregateException( | ||
| $"Process failed: {process.StartInfo.FileName} {string.Join(' ', process.StartInfo.ArgumentList)}\n" + | ||
| $"stdout: {output}\nstderr: {error}", failures); | ||
| } |
Aaronontheweb
force-pushed
the
feature/tool-recurrence
branch
5 times, most recently
from
October 11, 2026 16:14
f985e4f to
98426eb
Compare
The recurrence guard replaces the hard MaxToolIterationsPerTurn budget. Remove the demo AppHost env var, README prose, and the session-config-decomposition requirement that still referenced the removed key.
Aaronontheweb
force-pushed
the
feature/tool-recurrence
branch
from
October 12, 2026 02:20
98426eb to
ee2f744
Compare
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.
Hard tool-iteration limits stop useful long tasks. This change removes those limits and detects exact recurrence across feedback rounds.
The detector compares prepared call identities and trusted outcomes. Two equal rounds trigger correction. A further prohibited recurrence stops the batch before dispatch. Changed outcomes and authorized pending-job checks can continue. Approval prompts, cancellation, operation health checks, and the adjacent-cycle guard remain.
Durable task evidence survives compaction, restart, approval redrive, and attributed job results. Recovery preserves committed sibling results and closes unanswered calls with truthful non-success results. The model decides further work from recorded facts. The framework does not replay uncertain effects or request another model response for terminal settlement.
Subscriber death also remains safe across synchronous journal writes. Subscribers use raw death notifications because persistence can lose custom death-watch messages. Explicit leave removes the watch. Tests stop real subscribers during a held journal write and verify durable turn completion and idle passivation.
This is the first PR in native GitHub stack #2423. Its base is
dev. Dependent drafts are #2421 and #2422. The delivery plan records contracts and evidence limits.Why this is safe: Trusted prepared identities and actual outcomes govern recurrence. Durable task adoption retains the original authority. Terminal settlement requires no additional model response. Private replay and productive holdout gates remain mandatory.
Validation:
devatba37133e.DeathPactException.Unwatchfails both explicit-leave controls. Independent reviews confirm the persistence and actor-message cause.fff88fc47and0120c5b53. These remain revision-scoped evidence.5fe92fe2.3dd2c1a3records 34 original passes from 35 trials. The stricter JSON contract yields 33 passes. One compaction trial adds a forbidden effect; another uses fenced JSON.f6685d5f, with independent review. Two Missing reads precede child completion. Two later reads return the actual file; the next identical read receives a guard correction. This proves the specific regression, not general model compliance. Private replay, shadow review, and the productive holdout still require separate acceptance.The actual SQLite plugin rejects new
tta-v1records through the original reader before effects. A disposable backup restoration retains the original events, snapshot, and job artifact. This proof excludes live rollback and cannot undo external effects.This PR remains a draft. Private replay, shadow, and the 3,000-turn independent productive holdout remain open. No faithful replay or independently labeled productive holdout turn is certified. Exact recurrence does not detect every semantic loop or guarantee finite total spend.
No merge, deployment, or skill publication is included.