diff --git a/docs/LAB.md b/docs/LAB.md
index 300c0d5..270c9eb 100644
--- a/docs/LAB.md
+++ b/docs/LAB.md
@@ -103,6 +103,10 @@ machine's own git credentials. Every other request (spec generation, the 5-agent
answer-only. It is still code from a ticket running on your box; the machine's owner can refuse scoped runs
with `REMOTE_AGENT_AUTONOMY=off`. With the runner paired, `RUN_AGENTOS_E2E_REAL=true AGENTOS_REAL_URL=https://lab.dev.localhost:6180 AGENTOS_E2E_RUN_TIMEOUT_SECONDS=1200 dotnet test --project tests/AgentOs.E2E.Tests --filter-class "*WorkflowAppRealAuthTests"` runs the full 5-agent pipeline live.
+The runner keeps two lanes: work runs (scoped: clone, build, push — `REMOTE_AGENT_MAX_PARALLEL`, default 2) and
+answer-only runs such as a New idea spec or ticket split (`REMOTE_AGENT_MAX_ANSWERS`, default 2), so a person waiting on
+the screen never queues behind Autopilot's coding. Specialist answers get a 5-minute deadline (`LlmRequest.AgentAnswerTimeout`).
+
### Try the app on the lab
With the runner paired, Board → VNChessAcademy → **Try the app** on a repository runs its latest default branch on
diff --git a/src/AgentOs.Domain/Llm/LlmRequest.cs b/src/AgentOs.Domain/Llm/LlmRequest.cs
index 1a602f4..49efc94 100644
--- a/src/AgentOs.Domain/Llm/LlmRequest.cs
+++ b/src/AgentOs.Domain/Llm/LlmRequest.cs
@@ -44,6 +44,12 @@ public sealed record LlmRequest(
System.TimeSpan? Timeout = null,
string? Cli = null)
{
+ ///
+ /// Deadline for one specialist answer (a spec, a ticket split, a review) on a paired computer: a CLI answer takes a
+ /// minute or two, more on a busy machine. Above the 120 s dispatch default, far below a coding run's budget.
+ ///
+ public static readonly System.TimeSpan AgentAnswerTimeout = System.TimeSpan.FromMinutes(5);
+
///
/// Optional canonical agent role behind this request ("Requirement"/"Coding"/"Testing"/
/// "Qa"), set by the specialist agents. Lets a provider identify the agent WITHOUT parsing the system
diff --git a/src/AgentOs.Modules.Pipeline/Agents/LlmAgentBase.cs b/src/AgentOs.Modules.Pipeline/Agents/LlmAgentBase.cs
index 3ed911e..c047803 100644
--- a/src/AgentOs.Modules.Pipeline/Agents/LlmAgentBase.cs
+++ b/src/AgentOs.Modules.Pipeline/Agents/LlmAgentBase.cs
@@ -112,6 +112,7 @@ protected async Task ExecuteAsync(
// Carry the fixed agent role out-of-band so providers (notably the offline one) can identify the
// agent without parsing the system prompt — robust against per-tenant prompt overrides.
AgentRole = PromptKey,
+ Timeout = LlmRequest.AgentAnswerTimeout,
};
var response = await llm.SendAsync(request, ct).ConfigureAwait(false);
diff --git a/src/AgentOs.Modules.Pipeline/Decomposition/IdeaBootstrapService.cs b/src/AgentOs.Modules.Pipeline/Decomposition/IdeaBootstrapService.cs
index dc49be5..d13ae84 100644
--- a/src/AgentOs.Modules.Pipeline/Decomposition/IdeaBootstrapService.cs
+++ b/src/AgentOs.Modules.Pipeline/Decomposition/IdeaBootstrapService.cs
@@ -16,7 +16,12 @@ namespace AgentOs.Modules.Pipeline.Decomposition;
/// A generated requirement spec plus the tickets decomposed from it — the wizard's preview.
/// The structured requirement the idea produced.
/// The right-sized tickets to create (still editable before the user confirms).
-public sealed record BootstrapPreview(RequirementSpec Spec, IReadOnlyList Tickets);
+public sealed record BootstrapPreview(RequirementSpec Spec, IReadOnlyList Tickets)
+{
+ /// False when the Decomposition agent couldn't refine the tickets (unreachable, timed out, unreadable answer)
+ /// and these are the deterministic drafts — one per requirement line. The wizard says so and offers to try again.
+ public bool Refined { get; init; } = true;
+}
/// Turns a free-text idea into a reviewable spec + ticket set (no side effects).
public interface IIdeaBootstrapService
@@ -48,6 +53,7 @@ public async Task GenerateAsync(string idea, string locale = "
var seed = TicketMapper.Map(spec);
var tickets = await _decomposer.RunAsync(spec, seed, idea, cancellationToken).ConfigureAwait(false);
- return new BootstrapPreview(spec, tickets);
+ // The decomposer hands back the very seed list when it couldn't refine it.
+ return new BootstrapPreview(spec, tickets) { Refined = !ReferenceEquals(tickets, seed) };
}
}
diff --git a/src/AgentOs.Modules.Pipeline/Decomposition/TicketDecomposerAgent.cs b/src/AgentOs.Modules.Pipeline/Decomposition/TicketDecomposerAgent.cs
index 453d988..6756416 100644
--- a/src/AgentOs.Modules.Pipeline/Decomposition/TicketDecomposerAgent.cs
+++ b/src/AgentOs.Modules.Pipeline/Decomposition/TicketDecomposerAgent.cs
@@ -78,7 +78,8 @@ public async Task> RunAsync(RequirementSpec spec, IRe
UserPrompt: DecomposerPrompt.RenderUser(spec, seed, idea),
Model: profile.Model,
Temperature: profile.Temperature,
- MaxTokens: profile.MaxTokens);
+ MaxTokens: profile.MaxTokens,
+ Timeout: LlmRequest.AgentAnswerTimeout);
try
{
diff --git a/src/AgentOs.RemoteAgent/Program.cs b/src/AgentOs.RemoteAgent/Program.cs
index 766982a..820fa0f 100644
--- a/src/AgentOs.RemoteAgent/Program.cs
+++ b/src/AgentOs.RemoteAgent/Program.cs
@@ -29,7 +29,9 @@
// REMOTE_AGENT_ALLOW shell allowlist for server-driven tool calls (see RunnerCommandPolicy): a comma list
// replaces the defaults, "+name" adds, "*" allows everything (explicit opt-out)
// REMOTE_AGENT_TIMEOUT_SECONDS per shell command (default 600); on expiry the whole process tree is killed
-// REMOTE_AGENT_MAX_PARALLEL CLI runs at once on this machine (default 2); more wait their turn
+// REMOTE_AGENT_MAX_PARALLEL work runs (scoped: clone, build, push) at once on this machine (default 2); more wait
+// REMOTE_AGENT_MAX_ANSWERS answer-only runs (a spec, a ticket split — someone is waiting on screen) at once
+// (default 2); their own lane, so they never queue behind a long work run
// REMOTE_AGENT_DATA test data the agents may read: "NAME=C:\path;OTHER=/data/x" → env vars of each scoped run,
// names (not paths) told to the agent, read-only to it (see TestData in ScopedAutonomy.cs)
// REMOTE_AGENT_PREVIEWS "off" refuses "Try the app" previews on this machine (default: allowed — the member clicks
@@ -107,14 +109,20 @@
// So handlers only enqueue; the work runs off the receive loop, at most REMOTE_AGENT_MAX_PARALLEL CLI runs at once.
var maxParallel = int.TryParse(Environment.GetEnvironmentVariable("REMOTE_AGENT_MAX_PARALLEL"), out var mp) && mp > 0 ? mp : 2;
var cliSlots = new SemaphoreSlim(maxParallel, maxParallel);
+// Answer-only requests (no tools on this machine) are short and usually have a person waiting on them. Behind two
+// 15-minute coding runs they outlived the server's deadline — the ticket split fell back to the raw draft list.
+var maxAnswers = int.TryParse(Environment.GetEnvironmentVariable("REMOTE_AGENT_MAX_ANSWERS"), out var ma) && ma > 0 ? ma : 2;
+var answerSlots = new SemaphoreSlim(maxAnswers, maxAnswers);
// M3 — full-prompt LLM dispatch
connection.On("Execute", request =>
{
Console.WriteLine($"[agent] Execute {request.Id} (model={request.Model})");
+ var lane = AgentOs.RemoteAgent.ScopedAutonomy.Grants(request.Autonomy, Environment.GetEnvironmentVariable("REMOTE_AGENT_AUTONOMY"))
+ ? cliSlots : answerSlots;
_ = Task.Run(async () =>
{
- await cliSlots.WaitAsync().ConfigureAwait(false);
+ await lane.WaitAsync().ConfigureAwait(false);
try
{
// Stopped while it waited for a slot: never start it.
@@ -135,7 +143,7 @@
}
finally
{
- cliSlots.Release();
+ lane.Release();
}
});
return Task.CompletedTask;
diff --git a/src/AgentOs.Web/Components/Pages/BoardApp.razor b/src/AgentOs.Web/Components/Pages/BoardApp.razor
index b28c084..c316c8c 100644
--- a/src/AgentOs.Web/Components/Pages/BoardApp.razor
+++ b/src/AgentOs.Web/Components/Pages/BoardApp.razor
@@ -461,6 +461,18 @@ else if (_tab == Tab.Idea)
@_preview.Spec.Title
@_preview.Spec.Summary
+ @if (!_preview.Refined)
+ {
+
+ The AI couldn't split this into tickets this time.
+ These are rough drafts, one per requirement line — try again (your computer may have been busy), or edit and remove rows below.
+
+
+ @(_ideaBusy ? "Trying…" : "Try again")
+
+
+
+ }
diff --git a/tests/AgentOs.Tests/Decomposition/IdeaBootstrapServiceTests.cs b/tests/AgentOs.Tests/Decomposition/IdeaBootstrapServiceTests.cs
index b32b2bc..bcd396a 100644
--- a/tests/AgentOs.Tests/Decomposition/IdeaBootstrapServiceTests.cs
+++ b/tests/AgentOs.Tests/Decomposition/IdeaBootstrapServiceTests.cs
@@ -36,6 +36,24 @@ await requirement.Received(1).RunAsync(
// The decomposer is fed the deterministic seed (non-empty) built from the spec.
await decomposer.Received(1).RunAsync(
spec, Arg.Is>(seed => seed!.Count > 0), "build a product catalog", Arg.Any());
+ preview.Refined.ShouldBeTrue();
+ }
+
+ [Fact]
+ public async Task GenerateAsync_DecomposerFellBackToSeed_IsNotRefined()
+ {
+ var spec = DecompositionFixtures.Spec();
+ var requirement = Substitute.For();
+ requirement.RunAsync(Arg.Any(), Arg.Any()).Returns(spec);
+ var decomposer = Substitute.For();
+ // The real agent returns the very seed list it was given when it can't refine it.
+ decomposer.RunAsync(spec, Arg.Any>(), Arg.Any(), Arg.Any())
+ .Returns(ci => ci.ArgAt>(1));
+
+ var preview = await new IdeaBootstrapService(requirement, decomposer).GenerateAsync("build a product catalog");
+
+ preview.Refined.ShouldBeFalse();
+ preview.Tickets.ShouldNotBeEmpty();
}
[Fact]