Skip to content

fix: using declarations stay statements, snippet threads stop with their run, host cap for timeoutMs - #9

Merged
IvanMurzak merged 5 commits into
mainfrom
fix/w6-using-threads-timeout
Oct 29, 2026
Merged

IvanMurzak merged 5 commits into
mainfrom
fix/w6-using-threads-timeout

Conversation

@IvanMurzak

Copy link
Copy Markdown
Owner

Fixes from wave-6 testing (Godot/DeepSeek). No release, no version bump, no workflow changes.

1. using var x = ...; as a snippet statement (C#)

Script parsing took a top-level using declaration for a using directive (CS1002/CS0825/CS0103), also as the first line.
SnippetWriter now finds top-level [await] using declarations by token scan (Type name = after using), blanks the
keywords before parsing (positions stay) and writes the agent's own text back. using X;, using static, using unsafe,
aliases and global using stay directives; invalid forms keep their own error. global using now goes before the
runtime's usings (was CS8915).

2. Threads and tasks a snippet starts stop with their run

  • C#: delegates handed to new Thread, ThreadPool.(Unsafe)QueueUserWorkItem, Task.Run, TaskFactory.StartNew, new Task are
    wrapped (Worker / TaskWorker, cast to the delegate type they already convert to, so overloads stay). The work belongs to
    the run, including asynchronous task work across awaits and the threads and tasks it starts in turn. After the run it
    stops at its next guard check. Thread entries swallow the stop, tasks fault. Found on the way: before this change, a timeout stop on a
    thread the snippet started was an unhandled exception that ended a CoreCLR host (test StoppedThreadsNeverEndTheProcess).
    A warning reports unfinished work. A stop in work the snippet waited for is a Timeout or Runtime error.
  • Python: threads started from the snippet's thread (or its threads) are tracked through a Thread.start dispatcher
    installed only while a snippet runs. At the end, timers not yet fired are cancelled. Other threads get
    SnippetThreadStopped (async exception) while the agent's code is their running frame. A warning counts them.
  • Not covered (spec §3.6 / §4): timers and callbacks, async void code after its first await, waits and host calls, native threads,
    _thread.start_new_thread.

3. Timeout ergonomics

Hosts can cap timeoutMs: C# CodeRunnerOptions.MaxTimeoutMs, Python code_exec.set_max_timeout_ms. A longer budget, or none,
becomes the cap. The spec recommends exposing timeoutMs as a timeout tool argument, with a default of about 30 s and a cap
below the tool timeout.

Quality stages

  • simplify: 9ddee5e
  • code review: 4f833a0 (three reviewers: using/SnippetWriter, C# thread guard, Python threads)
  • perf: 3d6659b, reviewed afterwards; that review's fix is code review: d5a812d

Overhead

  • About +0.2–0.5 µs per Task.Run (1.0–1.4 µs unguarded); thread start is unchanged within noise.
  • Compile time is about +1 ms when a snippet starts work.
  • No per-check cost unless a run's work is still executing after the run ended.

Local verification

  • ROSLYN_VERSIONS="3.8.0 5.9.0" scripts/test-all.sh is green: net8/net10 267 tests, SourceConsumer, pack + consumer, Python 60.
  • net48: 254 tests.
  • Python 3.10, 3.11 and 3.14: green.

🤖 Generated with Claude Code

IvanMurzak and others added 5 commits October 29, 2026 00:31
…eir run, host cap for timeoutMs

- C#: a top-level using declaration (`using var x = ...;`, `using T x = ...;`, `await using ...`) was parsed as a
  using directive in script code (CS1002/CS0825/CS0103). It is now a statement wherever it stands; `using X;`,
  `using static`, `using unsafe`, aliases and `global using` stay directives. `global using` goes before the runtime's
  usings (CS8915 before).
- C#: delegates handed to new Thread / ThreadPool.QueueUserWorkItem / Task.Run / TaskFactory.StartNew / new Task are
  wrapped so the work belongs to the run and stops at its next guard check once the run has ended. A thread entry
  swallows the stop: before, a time stop on a snippet's thread was an unhandled exception that ended a CoreCLR host.
  A warning reports unfinished work; a budget spent in work the snippet waited for is a Timeout.
- Python: threads the snippet starts are stopped when it ends (async exception while they run the agent's code),
  threading.Timer not fired yet is cancelled, a warning counts them.
- Both: hosts can cap timeoutMs (CodeRunnerOptions.MaxTimeoutMs, code_exec.set_max_timeout_ms); spec recommends
  exposing a timeout argument with a ~30 s default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rd's nested closures from stack traces

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C#
- Work carries its run in a thread-static while synchronous and in an AsyncLocal only for asynchronous task work, so
  timers and async void continuations started from a worker no longer inherit it (an orphan stop there ended the
  process).
- Asynchronous task work stays pending until its task ends; End turns on orphan checks whenever the run started work
  (fenced against Spawn), so a continuation after the run ended is stopped and reported.
- Thread entries (Worker) swallow stops, task entries (TaskWorker) fault: a void task no longer completes after a stop.
- A stack stop in a thread the snippet waited for is a Runtime error, not success.
- Using declarations: a keyword after `using` or a type with two names in a row is no declaration; a detected
  declaration that does not become a statement keeps its `using` (re-parse), so invalid code keeps its own error.

Python
- One Thread.start dispatcher installed while any snippet runs (nested runs on snippet threads restore correctly);
  the runner's own threads start untracked; stop() never raises and runs after flushing logs.
- A stop is raised only while the agent's code is the running frame, never once the thread is leaving; cancelled
  timers are not counted; a thread started twice is tracked once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Local only once async task work ran

After a run that started work, a reused (Mono) assembly paid a thread-static or AsyncLocal read on every clock
check: fib(27) went from 1.5 ms to 2.8 ms. Now the check is skipped while no work of any run is executing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unwrapped continuations carry the run)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IvanMurzak
IvanMurzak merged commit 2556a2f into main Oct 29, 2026
13 checks passed
@IvanMurzak
IvanMurzak deleted the fix/w6-using-threads-timeout branch October 29, 2026 08:41
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