Skip to content

Commit 5675663

Browse files
committed
fix: refresh tool receipt deadlines on execution progress
Use the latest progress timestamp to extend receipt waits and recheck concurrent progress before expiring a tool call. Preserve run timeouts and late-result protection.
1 parent 9ce4cd8 commit 5675663

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

‎internal/service/execution.go‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,20 +535,31 @@ func (s *Service) waitToolResult(ctx context.Context, run *domain.Run, call *dom
535535
if err != nil && !errors.Is(err, ports.ErrNotFound) {
536536
return agentruntime.ToolObservation{}, err
537537
}
538+
// Progress is a receipt too. Executor contexts and the run context bound
539+
// the operation; this timer only detects an executor that stopped replying.
540+
if result != nil && !result.Done && result.Status == "running" {
541+
deadline.Reset(time.Until(result.CreatedAt.Add(wait)))
542+
}
538543
select {
539544
case <-ctx.Done():
540545
return agentruntime.ToolObservation{}, ctx.Err()
541546
case <-deadline.C:
542-
return s.expireToolWait(ctx, run, call)
547+
observation, expireErr := s.expireToolWait(ctx, run, call, wait)
548+
if errors.Is(expireErr, errToolWaitProgress) {
549+
continue
550+
}
551+
return observation, expireErr
543552
case <-notifications:
544553
case <-ticker.C:
545554
}
546555
}
547556
}
548557

558+
var errToolWaitProgress = errors.New("tool progress advanced the receipt deadline")
559+
549560
// A lost executor receipt is an observation for the model, never permission to
550561
// retry a write. The transaction arbitrates with a concurrently arriving result.
551-
func (s *Service) expireToolWait(ctx context.Context, run *domain.Run, call *domain.ToolCall) (agentruntime.ToolObservation, error) {
562+
func (s *Service) expireToolWait(ctx context.Context, run *domain.Run, call *domain.ToolCall, wait time.Duration) (agentruntime.ToolObservation, error) {
552563
var observation agentruntime.ToolObservation
553564
now := time.Now().UTC()
554565
err := s.store.Transaction(ctx, func(tx ports.Tx) error {
@@ -560,6 +571,9 @@ func (s *Service) expireToolWait(ctx context.Context, run *domain.Run, call *dom
560571
if err != nil && !errors.Is(err, ports.ErrNotFound) {
561572
return err
562573
}
574+
if latest != nil && latest.Status == "running" && now.Before(latest.CreatedAt.Add(wait)) {
575+
return errToolWaitProgress
576+
}
563577
current, err := tx.RunInternal(run.ID, true)
564578
if err != nil {
565579
return err

‎internal/service/tool_timeout_test.go‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,41 @@ package service
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"testing"
78
"time"
89
)
910

11+
func TestReceiptExpiryRechecksConcurrentProgress(t *testing.T) {
12+
s, principal, run, call, approval := approvalFixture(t)
13+
ctx := context.Background()
14+
if _, _, err := s.DecideApproval(ctx, principal, approval.ID, ApprovalDecisionRequest{Decision: "approve"}); err != nil {
15+
t.Fatal(err)
16+
}
17+
if err := s.dispatchToolCall(ctx, run, call); err != nil {
18+
t.Fatal(err)
19+
}
20+
_, _, err := s.SubmitToolResult(ctx, principal, call.ID, ToolResultRequest{
21+
RunID: run.ID, Sequence: 1, Status: "running",
22+
Result: json.RawMessage(`{"status":"awaiting_approval"}`),
23+
})
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
if _, err = s.expireToolWait(ctx, run, call, 45*time.Second); !errors.Is(err, errToolWaitProgress) {
28+
t.Fatalf("concurrent review progress was expired: %v", err)
29+
}
30+
if _, _, err = s.SubmitToolResult(ctx, principal, call.ID, ToolResultRequest{
31+
RunID: run.ID, Sequence: 2, Done: true, Status: "success", Result: json.RawMessage(`{"output":"files"}`),
32+
}); err != nil {
33+
t.Fatal(err)
34+
}
35+
observation, err := s.waitToolResult(ctx, run, call)
36+
if err != nil || observation.Status != "success" {
37+
t.Fatalf("lost completion after approval: %+v %v", observation, err)
38+
}
39+
}
40+
1041
func TestToolReceiptDeadlineReturnsObservationAndRejectsLateResult(t *testing.T) {
1142
s, principal, run, call, approval := approvalFixture(t)
1243
s.toolResultTimeout = 10 * time.Millisecond

0 commit comments

Comments
 (0)