Files
goclaw/internal/tools/async_completion_delivery.go
Duc Nguyenandntduc bb7712a9ff fix(collaboration): harden delegated task isolation (#1486)
* feat(collaboration): isolate delegated artifacts and child runs

Isolate delegated inputs and outputs behind secure artifact exchange lifecycles. Scope Agent Link tasks by tenant and root agent, and enforce delegation spawn-tree boundaries. Add process-wide child-run admission and preserve logical media paths across native, MCP, and sandbox execution.

* fix(collaboration): harden delegated task isolation

Enforce tenant and root-agent task scope across migrations and stores. Add exactly-once async completion delivery, delegated sandbox boundaries, and confined artifact and media recovery across runtime surfaces.

* fix(collaboration): recover interrupted async tasks

* fix(collaboration): normalize persisted child-run status

---------

Co-authored-by: ntduc <ntduc@cpp.ai.vn>
2026-07-30 14:17:40 +07:00

94 lines
2.0 KiB
Go

package tools
import (
"context"
"time"
"github.com/nextlevelbuilder/goclaw/internal/bus"
)
const asyncCompletionAttempts = 3
const (
asyncTerminalPersistenceAttempts = 12
asyncPersistenceAttemptTimeout = 2 * time.Second
asyncPersistenceMaxBackoff = time.Second
)
// PublishAsyncCompletion retries only after child execution has released its
// admission lease. The durable task row remains the fallback if every attempt
// finds the inbound queue saturated.
func PublishAsyncCompletion(
ctx context.Context,
msgBus *bus.MessageBus,
message bus.InboundMessage,
) bool {
if msgBus == nil {
return true
}
if ctx == nil {
ctx = context.Background()
}
for attempt := range asyncCompletionAttempts {
if msgBus.TryPublishInbound(message) {
return true
}
if attempt == asyncCompletionAttempts-1 {
break
}
timer := time.NewTimer(time.Duration(attempt+1) * 10 * time.Millisecond)
select {
case <-ctx.Done():
timer.Stop()
return false
case <-timer.C:
}
}
return false
}
func retryAsyncPersistence(
ctx context.Context,
operation func(context.Context) error,
) error {
return retryPersistence(ctx, asyncCompletionAttempts, operation)
}
func retryTerminalPersistence(
ctx context.Context,
operation func(context.Context) error,
) error {
return retryPersistence(ctx, asyncTerminalPersistenceAttempts, operation)
}
func retryPersistence(
ctx context.Context,
attempts int,
operation func(context.Context) error,
) error {
if ctx == nil {
ctx = context.Background()
}
var err error
for attempt := range attempts {
attemptCtx, cancel := context.WithTimeout(ctx, asyncPersistenceAttemptTimeout)
err = operation(attemptCtx)
cancel()
if err == nil {
return nil
}
if attempt == attempts-1 {
break
}
backoff := min(time.Duration(1<<attempt)*25*time.Millisecond, asyncPersistenceMaxBackoff)
timer := time.NewTimer(backoff)
select {
case <-ctx.Done():
timer.Stop()
return err
case <-timer.C:
}
}
return err
}