feat(worker): surface the handoff round budget in the merge-helper handoff brief
A handler session only learned the chain was capped when handoff_list_handler threw at the end of its work, stranding open tasks. The round count already lived in HandoffRoundTracker the whole time -- it just never reached the brief. InteractiveLaunchSpecService now reads HandoffRoundTracker.GetState (read-only, doesn't advance the round) and renders round N of MaxHandoffs plus the remaining count into the handoff brief, replacing the old one-line finalNote. The rejection error also names the next action (start a new handler run) instead of just failing.
This commit is contained in:
@@ -4,6 +4,7 @@ using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.External;
|
||||
using ClaudeDo.Worker.Planning;
|
||||
using ClaudeDo.Worker.Skills;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -42,6 +43,7 @@ public sealed class InteractiveLaunchSpecService
|
||||
private readonly WorktreeManager _wtManager;
|
||||
private readonly GitService _git;
|
||||
private readonly string _claudePath;
|
||||
private readonly HandoffRoundTracker _handoffRounds;
|
||||
|
||||
public InteractiveLaunchSpecService(
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
@@ -49,7 +51,8 @@ public sealed class InteractiveLaunchSpecService
|
||||
ISessionSkillRegistry skillRegistry,
|
||||
WorktreeManager wtManager,
|
||||
GitService git,
|
||||
WorkerConfig cfg)
|
||||
WorkerConfig cfg,
|
||||
HandoffRoundTracker handoffRounds)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_skillSeeder = skillSeeder;
|
||||
@@ -57,6 +60,7 @@ public sealed class InteractiveLaunchSpecService
|
||||
_wtManager = wtManager;
|
||||
_git = git;
|
||||
_claudePath = cfg.ClaudeBin;
|
||||
_handoffRounds = handoffRounds;
|
||||
}
|
||||
|
||||
/// <summary>Builds a LaunchSpec for opening an interactive session in a task's worktree.
|
||||
@@ -393,13 +397,19 @@ public sealed class InteractiveLaunchSpecService
|
||||
var systemPromptPath = Path.Combine(sessionDir, "system-prompt.md");
|
||||
await File.WriteAllTextAsync(systemPromptPath, PromptFiles.ReadOrDefault(phase.PromptKind), ct);
|
||||
|
||||
// A "_final" round differs from its non-final counterpart in exactly one rendered line --
|
||||
// the same system prompt and model drive both -- marking the final round and forbidding a
|
||||
// further rerun, so the chain is bounded structurally rather than by prompt appeal alone.
|
||||
var finalNote = phase.IsFinal
|
||||
? "\nThis is the FINAL round of this list-handler run -- do not start any further reruns; " +
|
||||
"merge what you can, report the rest, and print the summary."
|
||||
: "";
|
||||
// The round count/cap come from HandoffRoundTracker -- the same instance HandoffMcpTools
|
||||
// registered this handoff against -- never recomputed here, so the brief can't drift from
|
||||
// the server-side cap that actually enforces the chain. A "_final" round adds one more line
|
||||
// forbidding a further rerun, so the chain is bounded structurally rather than by prompt
|
||||
// appeal alone.
|
||||
var round = _handoffRounds.GetState(taskId);
|
||||
var remaining = Math.Max(HandoffRoundTracker.MaxHandoffs - round.Count, 0);
|
||||
var roundInfo = $"Handoff budget: this is round {round.Count} of " +
|
||||
$"{HandoffRoundTracker.MaxHandoffs} possible handoffs for this list-handler run " +
|
||||
$"({remaining} more possible after this one).";
|
||||
if (phase.IsFinal)
|
||||
roundInfo += "\nThis is the FINAL round of this list-handler run -- do not start any further reruns; " +
|
||||
"merge what you can, report the rest, and print the summary.";
|
||||
|
||||
var briefPath = Path.Combine(sessionDir, "handoff.md");
|
||||
await File.WriteAllTextAsync(briefPath, PromptFiles.Render(PromptKind.MergeHelperHandoff,
|
||||
@@ -409,7 +419,7 @@ public sealed class InteractiveLaunchSpecService
|
||||
["repo"] = repoDir,
|
||||
["handlerTaskId"] = taskId,
|
||||
["tasks"] = RenderGroupedBrief(briefEntries),
|
||||
["finalNote"] = finalNote,
|
||||
["roundInfo"] = roundInfo,
|
||||
}), ct);
|
||||
|
||||
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
||||
|
||||
Reference in New Issue
Block a user