Detach prepared execution ownership from runners

This commit is contained in:
2026-11-05 13:52:36 +00:00
parent 7d500ee62f
commit 2f0f5fa607
5 changed files with 23 additions and 4 deletions

View File

@@ -266,6 +266,10 @@ same-engine views, one-shot/concurrent claim/discard and details after use.
Session Unicode/trim/content/wire/hash tests stay unchanged in meaning. Reference
analysis, not a GC-timing assertion, establishes that the token retains no runner.
Completion: prepared handles retain only a private nonzero-sized runner identity
token, including after use or discard; accepted session strings own their
storage. Focused prepared/session race tests pass on Go 1.27.3.
## Stage 10: Declare Validation Preparation As The Internal Dependency
**Catalog:** 21, interface part. **Files:** validator constructors, engine Options,

View File

@@ -18,5 +18,5 @@ func NormalizeSessionID(raw string) (string, error) {
if length := utf8.RuneCountInString(normalized); length > SessionIDMaxLength {
return "", fmt.Errorf("session_id length %d exceeds maximum %d", length, SessionIDMaxLength)
}
return normalized, nil
return strings.Clone(normalized), nil
}

View File

@@ -22,7 +22,7 @@ const (
// PreparedExecution owns one frozen, single-use runner execution.
type PreparedExecution struct {
owner *Runner
owner *runnerIdentity
mu sync.Mutex
state preparedExecutionState
details *domain.PreparedRun
@@ -63,7 +63,7 @@ func (r *Runner) PrepareExecution(ctx context.Context, req domain.RunRequest) (*
}
return &PreparedExecution{
owner: r,
owner: r.identity,
state: preparedExecutionReady,
details: details,
payload: &preparedExecutionPayload{
@@ -170,7 +170,7 @@ func (r *Runner) RunPrepared(ctx context.Context, prepared *PreparedExecution) (
}
func (p *PreparedExecution) claim(owner *Runner) (*preparedExecutionPayload, error) {
if p == nil || owner == nil || p.owner != owner {
if p == nil || owner == nil || owner.identity == nil || p.owner != owner.identity {
return nil, fmt.Errorf("%w: prepared execution does not belong to this runner", ErrInvalidRequest)
}

View File

@@ -600,6 +600,9 @@ func TestRunnerPreparedExecutionOwnershipUseAndDiscard(t *testing.T) {
}
owner := newRunner()
if other := newRunner(); owner.identity == nil || owner.identity == other.identity {
t.Fatal("constructed runners must have independent identity tokens")
}
prepared, err := owner.PrepareExecution(context.Background(), request)
if err != nil {
t.Fatalf("prepare execution: %v", err)
@@ -616,6 +619,9 @@ func TestRunnerPreparedExecutionOwnershipUseAndDiscard(t *testing.T) {
if prepared.Details() == nil {
t.Fatal("details unavailable after execution")
}
if prepared.owner != owner.identity || prepared.payload != nil {
t.Fatal("claimed handle retained payload or lost owner identity")
}
discarded, err := owner.PrepareExecution(context.Background(), request)
if err != nil {
@@ -623,6 +629,9 @@ func TestRunnerPreparedExecutionOwnershipUseAndDiscard(t *testing.T) {
}
discarded.Discard()
discarded.Discard()
if discarded.owner != owner.identity || discarded.payload != nil {
t.Fatal("discarded handle retained payload or lost owner identity")
}
if _, err := owner.RunPrepared(context.Background(), discarded); !errors.Is(err, ErrInvalidRequest) {
t.Fatalf("discarded execution error=%v, want ErrInvalidRequest", err)
}

View File

@@ -43,6 +43,7 @@ var (
// Runner executes the Promptkit core use case.
type Runner struct {
identity *runnerIdentity
promptDefs promptdef.Repository
profiles profile.Repository
artifacts artifact.Reader
@@ -54,6 +55,10 @@ type Runner struct {
defaultTransientRetries uint64
}
// A non-zero-sized token gives each constructed runner a distinct identity
// without retaining its collaborators in prepared handles.
type runnerIdentity struct{ marker byte }
// RunAdmitter reserves capacity for one resolved backend run.
type RunAdmitter interface {
Admit(context.Context, string) (func(), error)
@@ -113,6 +118,7 @@ func NewRunnerWithRepairer(
retries = defaultTransientRetries[0]
}
return &Runner{
identity: &runnerIdentity{},
promptDefs: promptDefs,
profiles: profiles,
artifacts: artifacts,