package dispatcher

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"log/slog"
	"strings"
	"sync"

	"github.com/google/uuid"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/api/exec"
	"github.com/tidbcloud/AgentTeam/desktop/internal/memoryskill"
	"github.com/tidbcloud/AgentTeam/desktop/internal/messagesend"
	"github.com/tidbcloud/AgentTeam/desktop/internal/metrics"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/promptcompiler"
	"github.com/tidbcloud/AgentTeam/desktop/internal/repositorylocalproxy"
	"github.com/tidbcloud/AgentTeam/desktop/internal/token"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

type agentteamLLMProxySessionIssuerProvider interface {
	LLMProxySessionIssuer(serverAgentID string, credentialKind ...string) agent.AgentTeamLLMProxySessionIssuer
}

func promptExecCommonActions(actions []exec.PromptAction) []promptcompiler.ExecGuidanceAction {
	out := make([]promptcompiler.ExecGuidanceAction, 0, len(actions))
	for _, action := range actions {
		out = append(out, promptcompiler.ExecGuidanceAction{Domain: action.Domain, Name: action.Name, ExampleJSON: action.ExampleJSON})
	}
	return out
}

// resolvedInstructionContext holds the prompt-facing products of MCP config
// resolution: skill documentation and the memory resume reminder string.
type resolvedInstructionContext struct {
	skillDocParts        types.SkillDocParts
	skillPlan            promptcompiler.SkillPlan
	exec                 promptcompiler.ExecCompileInput
	extension            promptcompiler.ExtensionCompileInput
	memoryResumeReminder string
	attachments          promptcompiler.AttachmentResolution
}

// promptMode controls which prompt compilation path is used.
type promptMode string

const (
	promptModeSession         promptMode = "session"          // normal start / resume / wake
	promptModeDeferred        promptMode = "deferred"         // deferred message replay
	promptModeContextRecovery promptMode = "context_recovery" // context window exceeded restart
)

var (
	checkpointMsgFreshSession = promptcompiler.RecoveryCheckpoint("fresh_start")
	checkpointMsgResume       = promptcompiler.RecoveryCheckpoint("resume")
	checkpointMsgRecovery     = promptcompiler.RecoveryCheckpoint("context_recovery")
	checkpointMsgDeferred     = promptcompiler.RecoveryCheckpoint("deferred")
)

// processLaunchContext collects everything needed to start an agent process.
// The three "start process" code paths (startSession, startDeferredContinuation,
// startContextRecoveryProcess) differ only in a few options encoded here; the
// bulk of the work is done by the shared resolve + launch methods.
type processLaunchContext struct {
	sess           *interactiveSession
	agent          agent.Config
	runtime        runtimeconfig.Runtime
	binding        conversationBinding
	msg            types.InboxMessage
	wake           *agent.WakeInfo
	requestContext context.Context

	// --- resolved by resolve() ---
	driver             drivercontract.Driver
	instructions       resolvedInstructionContext
	startCfg           drivercontract.LaunchConfig
	workflowCompaction *agent.WorkflowCompactionGate
	runtimePolicy      types.RuntimePolicySnapshot
	fingerprint        string
	agentToken         string
	// runtimeStartFingerprint captures only runtime-affecting launch inputs. It
	// is checked before reusing a live process for later deliveries.
	runtimeStartFingerprint string
	usesWorkspaceRootMemory bool
	promptCompileMetrics    []promptCompileMeasurement

	// --- options ---
	mode                       promptMode
	pendingBehind              int
	skipResumeCheck            bool
	checkpointMsg              string
	suppressIdleStartTurn      bool
	ownershipHandoffMessageID  string
	startReservation           *sessionStartReservation
	authoritativeStartIdentity bool
	detachStartCancellation    func() error

	// --- saved-state overrides (used by deferred / context-recovery paths) ---
	savedModel           string
	savedReasoningEffort string
	savedSessionID       string
}

// resolve populates the driver, resolved instructions, start config, and
// fingerprint
// fields.  It must be called before launch.
func (d *Dispatcher) resolve(ctx *processLaunchContext) error {
	sess := ctx.sess
	ag := ctx.agent
	rt := ctx.runtime
	binding := ctx.binding

	// --- driver ---
	var err error
	if hook, ok := overrideNewAgentDriver(); ok {
		ctx.driver, err = hook(rt.Type)
	} else {
		ctx.driver, err = agent.NewDriver(rt.Type)
	}
	if err != nil {
		return err
	}

	// --- MCP / instruction resolution ---
	mcpAgentID := sess.sessionKey
	if strings.TrimSpace(mcpAgentID) == "" {
		mcpAgentID = sess.agentID
	}
	tokenValue, err := token.Default().GenerateAgentTokenForServer(mcpAgentID, binding.channelID)
	if err != nil {
		return fmt.Errorf("generate api key: %w", err)
	}
	ctx.agentToken = tokenValue
	replaceSessionMCPToken(sess, tokenValue)
	mcps := d.resolveMCPConfigs(
		binding.channelID,
		mcpAgentID,
		binding.serverAgentID,
		firstNonEmpty(binding.serverSessionID, sess.agentID),
		tokenValue,
		rt.Type,
	)
	mcps = d.appendMemorySearchMCP(
		ctx.requestContext,
		mcps,
		mcpAgentID,
		sess.agentID,
		tokenValue,
	)
	builtinSkills := memoryBuiltinSkillsForMCPs(mcps)
	ctx.instructions = d.resolveInstructionContext(mcpAgentID, mcps, rt.Type)

	// Attachment preparation is part of compilation input. Resolving it before
	// fingerprinting and launch prevents a later runtime path from mutating a
	// rendered prompt string.
	var preparedAttachments []types.RuntimeAttachment
	if messageAttachments := ctx.msg.Attachments(); len(messageAttachments) > 0 {
		resolveCtx := ctx.requestContext
		if resolveCtx == nil {
			resolveCtx = context.Background()
		}
		preparedAttachments, err = d.resolveRuntimeAttachments(resolveCtx, binding, messageAttachments)
		if err != nil {
			slog.Warn("resolve initial runtime attachments failed; compiling failure guidance",
				"agent_id", sess.agentID,
				"message_id", strings.TrimSpace(ctx.msg.MessageID),
				"error", err)
			ctx.instructions.attachments = promptcompiler.AttachmentResolutionError(messageAttachments, err)
		} else {
			ctx.instructions.attachments = promptcompiler.AttachmentResolution{Prepared: preparedAttachments}
		}
	}

	// --- start config ---
	model := strings.TrimSpace(ctx.savedModel)
	if model == "" {
		model = strings.TrimSpace(ag.Model)
	}
	reasoningEffort := strings.TrimSpace(ctx.savedReasoningEffort)
	if reasoningEffort == "" {
		reasoningEffort = strings.TrimSpace(ag.ReasoningEffort)
	}
	ctx.runtimePolicy = currentAgentRuntimePolicy(ag, rt)
	permissionMode := strings.TrimSpace(ctx.runtimePolicy.PermissionMode)
	runtimeEnv, err := repositorylocalproxy.RuntimeEnvForLease(cloneStringMap(rt.Env))
	if err != nil {
		return err
	}
	runtimeEnv = clearTokenAnalysisEnv(runtimeEnv)
	runtimeEnv = clearExecRuntimeEnv(runtimeEnv)
	ctx.startCfg = drivercontract.LaunchConfig{
		AgentID:             sess.agentID,
		LaunchID:            newPromptMetricsLaunchID(),
		RuntimeType:         rt.Type,
		MCPs:                mcps,
		WorkDir:             sess.workDir,
		RuntimeStateDir:     agentRuntimeStateDir(sess.agentID),
		Binary:              rt.Binary,
		PermissionMode:      permissionMode,
		RuntimeDebugEnabled: ag.RuntimeDebugEnabled,
		Model:               model,
		SmallModel:          strings.TrimSpace(ag.SmallModel),
		ReasoningEffort:     reasoningEffort,
		Env:                 runtimeEnv,
		SkillDir:            profile.Default().Skills().Dir(),
		ManagedSkillDirs:    sess.managedSkillPaths,
		BuiltinSkills:       builtinSkills,
		SkillPlan:           ctx.instructions.skillPlan,
	}
	ctx.startCfg.Env, ctx.startCfg.AnalysisToken = d.withTokenAnalysisEnv(ctx.startCfg.Env, sess.agentID)
	if len(ctx.startCfg.SkillPlan.SourceSlots) == 0 {
		ctx.startCfg.SkillPlan.SourceSlots = append(ctx.startCfg.SkillPlan.SourceSlots,
			promptcompiler.SkillSourceSlot{Dir: ctx.startCfg.SkillDir, Priority: promptcompiler.SkillSourcePriorityProfile, Origin: "profile"},
		)
		for _, dir := range ctx.startCfg.ManagedSkillDirs {
			ctx.startCfg.SkillPlan.SourceSlots = append(ctx.startCfg.SkillPlan.SourceSlots,
				promptcompiler.SkillSourceSlot{Dir: dir, Priority: promptcompiler.SkillSourcePriorityManaged, Origin: "managed"},
			)
		}
	}
	if len(preparedAttachments) > 0 {
		if _, ok := ctx.driver.(drivercontract.RuntimeInputDeliveryDriver); ok {
			ctx.startCfg.PromptAttachments = preparedAttachments
		}
	}

	llmResolution, err := profile.ResolveAgentLLM(ag, rt.Type)
	if err != nil {
		return err
	}
	if ctx.startCfg.Model == "" && llmResolution.Model != "" {
		ctx.startCfg.Model = llmResolution.Model
	}
	if ctx.startCfg.ReasoningEffort == "" && llmResolution.ReasoningEffort != "" {
		ctx.startCfg.ReasoningEffort = llmResolution.ReasoningEffort
	}
	if llmResolution.Upstream != nil {
		upstream := *llmResolution.Upstream
		ctx.startCfg.Provider = &drivercontract.ProviderConfig{
			ID:             upstream.ID,
			Name:           upstream.Name,
			BaseURL:        strings.TrimSpace(upstream.BaseURL),
			APIKey:         strings.TrimSpace(upstream.APIKey),
			CredentialKind: strings.TrimSpace(ag.CredentialKind),
			EnvKey:         strings.TrimSpace(upstream.EnvKey),
			Headers:        upstream.Headers,
			WireAPIs:       append([]string(nil), upstream.WireAPIs...),
		}
	}
	execResolved, err := d.prepareExec(ctx)
	if err != nil {
		return err
	}
	if execResolved != nil && execResolved.catalog != nil {
		ctx.instructions.exec = promptcompiler.ExecCompileInput{
			Enabled: true,
			Guidance: promptcompiler.ExecGuidanceTemplateData{
				Domains:       execResolved.catalog.PromptDomains(),
				CommonActions: promptExecCommonActions(execResolved.catalog.PromptCommonActions()),
			},
		}
	}
	extensionSnapshot := exec.ExtensionSnapshot{}
	if execResolved != nil && execResolved.runtimeCatalog != nil {
		extensionSnapshot = execResolved.runtimeCatalog.ExtensionSnapshot()
	}
	if actions := extensionSnapshot.PromptGuidanceActions(); len(actions) > 0 {
		guidance := make([]promptcompiler.ExtensionCatalogGuidanceAction, 0, len(actions))
		for _, action := range actions {
			guidance = append(guidance, promptcompiler.ExtensionCatalogGuidanceAction{ExtensionName: action.ExtensionName, ActionName: action.ActionName})
		}
		ctx.instructions.extension = promptcompiler.ExtensionCompileInput{
			Enabled:  true,
			Guidance: promptcompiler.ExtensionCatalogGuidanceTemplateData{Actions: guidance},
		}
	}
	ctx.runtimeStartFingerprint = runtimeStartFingerprint(
		ag,
		rt,
		ctx.runtimePolicy,
		ctx.startCfg,
	)
	resumeID := strings.TrimSpace(ctx.savedSessionID)
	if resumeID == "" {
		resumeID = strings.TrimSpace(binding.serverSessionID)
	}
	if resumeID == "" && ctx.mode == promptModeSession {
		resumeID = loadLocalSessionResumeCandidate(sess.workDir)
	}

	// Historical resume state can decide whether a provider session may be
	// reused, but it must not override the current runtime policy. Evaluate the
	// policy guard before compiling resume-vs-fresh instructions so a policy
	// mismatch gets a true fresh-start fingerprint and prompt.
	if !ctx.skipResumeCheck && resumeID != "" {
		decision, err := guardPersistedResumeState(sess.workDir, ctx.runtimePolicy)
		if err != nil {
			return err
		}
		if decision.Action == sessionResumeGuardClearAndFreshStart {
			logResumeGuardDecision(sess.agentID, decision)
			resumeID = ""
		}
	}

	// --- fingerprint ---
	resuming := resumeID != ""
	if ctx.mode == promptModeContextRecovery {
		resuming = false
	}
	ctx.fingerprint, err = compileSessionInstructionsFingerprint(
		sess.workDir, ag, rt.Type, binding, ctx.msg,
		ctx.instructions,
		mcps,
		resuming,
	)
	if err != nil {
		return err
	}

	// --- session resume validation (skip for context recovery) ---
	if !ctx.skipResumeCheck {
		if resumeID != "" {
			ctx.startCfg.SessionID = d.validateSessionResume(
				sess.workDir, resumeID,
				string(rt.Type), d.serverURL, ctx.fingerprint,
			)
		}
	}

	// --- resolve compiled session ID ---
	effectiveBinding := binding
	effectiveBinding.serverSessionID = ctx.startCfg.SessionID
	resolvedSessionID, err := resolveCompiledSessionID(
		sess.workDir, ag, rt.Type, effectiveBinding, ctx.msg,
		ctx.instructions,
		mcps,
	)
	if err != nil {
		return err
	}
	ctx.startCfg.SessionID = resolvedSessionID
	effectiveBinding.serverSessionID = resolvedSessionID
	if ctx.mode == promptModeSession {
		ctx.startCfg.InitialDeliveryMessageID = strings.TrimSpace(ctx.msg.MessageID)
	}
	ctx.workflowCompaction = agent.NewWorkflowCompactionGate()
	suppressInitialTurn := shouldSuppressIdleStartTurnPrompt(ctx.suppressIdleStartTurn, ctx.binding, ctx.msg)
	initialTurnVisible := !suppressInitialTurn &&
		isWakeMessage(ctx.msg)
	if initialTurnVisible {
		ctx.workflowCompaction.ObserveRuntimeMessage(ctx.msg)
	}
	execCfg, err := d.activateExec(context.Background(), ctx, resolvedSessionID, execResolved)
	if err != nil {
		return err
	}
	ctx.startCfg.Exec = execCfg
	ctx.startCfg.Env = withExecRuntimeEnv(ctx.startCfg.Env, execCfg)
	// --- compile prompts ---
	var prompts compiledSessionPrompts
	switch ctx.mode {
	case promptModeSession:
		prompts, err = compileSessionPrompts(
			sess.workDir, ag, rt.Type, effectiveBinding, ctx.msg,
			ctx.instructions,
		)
	case promptModeDeferred, promptModeContextRecovery:
		prompts, err = compileDeferredContinuationPrompts(
			sess.workDir, ag, rt.Type, effectiveBinding, ctx.msg,
			ctx.instructions,
			ctx.pendingBehind,
		)
	}
	if err != nil {
		return err
	}
	ctx.usesWorkspaceRootMemory = compiledPromptsUseWorkspaceRootMemory(prompts)
	if suppressInitialTurn {
		prompts = suppressIdleStartTurnPrompts(prompts)
	}
	// The compiler returns one complete sealed launch carrier. Dispatcher code
	// retains and passes it through; it does not extract, join, or duplicate
	// semantic launch surfaces.
	ctx.startCfg.CompiledPrompt = prompts.Carrier
	ctx.startCfg.ContractCarrierFile = prompts.Carrier.LaunchSurface("contractCarrierFile")
	ctx.promptCompileMetrics = prompts.CompileMetrics
	if !sess.ephemeral {
		d.emitTrajectoryEntries(binding.channelID, binding.serverAgentID, ctx.startCfg.SessionID, prompts.PromptInputs)
	}

	return nil
}

func memoryBuiltinSkillsForMCPs(mcps []types.NamedMCPConfig) []drivercontract.BuiltinSkill {
	for _, server := range mcps {
		if strings.TrimSpace(server.Name) != "agentteam" {
			continue
		}
		name, content := memoryskill.Maintenance()
		return []drivercontract.BuiltinSkill{{Name: name, Content: content}}
	}
	return nil
}

// appendMemorySearchMCP exposes the local memory search endpoint. The tool is
// always advertised; when no index is available the call returns an actionable
// tool error instead of the tool being absent.
func (d *Dispatcher) appendMemorySearchMCP(
	ctx context.Context,
	mcps []types.NamedMCPConfig,
	routeAgentID, localAgentID, tokenValue string,
) []types.NamedMCPConfig {
	if reg, _ := d.snapshotMemorySearch(ctx); reg != nil {
		reg.bindRoute(routeAgentID, localAgentID)
	}
	return mcps
}

func loadLocalSessionResumeCandidate(workDir string) string {
	state, err := loadSessionResumeState(workDir)
	if err != nil {
		slog.Debug("load local session resume candidate", "work_dir", workDir, "error", err)
		return ""
	}
	if state.LastStartConfig != nil {
		if sessionID := strings.TrimSpace(state.LastStartConfig.SessionID); sessionID != "" {
			return sessionID
		}
	}
	return strings.TrimSpace(state.LastSessionID)
}

func shouldSuppressIdleStartTurnPrompt(suppress bool, _ conversationBinding, msg types.InboxMessage) bool {
	if !suppress {
		return false
	}
	if isWakeMessage(msg) {
		return false
	}
	// The caller decides whether unread summary should start an idle turn. In
	// particular, agent:start passes suppress=false when it intentionally wants
	// to process a non-empty summary. Once suppress=true reaches this boundary,
	// it may also be a durable ownership restore/handoff: that launch must stay
	// completely no-prompt until the ownership store is installed and the
	// Process is atomically published. Letting unreadSummary override the flag
	// would mutate the runtime before that durability boundary.
	return true
}

func suppressIdleStartTurnPrompts(prompts compiledSessionPrompts) compiledSessionPrompts {
	prompts.CompileMetrics = filterPromptCompileMeasurements(prompts.CompileMetrics, func(item promptCompileMeasurement) bool {
		return item.stage != "turn_context" && item.stage != "deferred_continuation"
	})
	filtered := make([]types.TrajectoryEntry, 0, len(prompts.PromptInputs))
	for _, entry := range prompts.PromptInputs {
		if strings.TrimSpace(entry.PromptKind) == "turn_input" || strings.TrimSpace(entry.PromptKind) == "deferred_continuation" {
			continue
		}
		filtered = append(filtered, entry)
	}
	prompts.PromptInputs = filtered
	return prompts
}

func sessionPromptCompileRecords(agentID, runtime, launchID, sessionKind, freshness string, items []promptCompileMeasurement) []metrics.PromptCompileRecord {
	var records []metrics.PromptCompileRecord
	for _, measurement := range items {
		if measurement.stage != "thread_contract" && measurement.freshness != freshness {
			continue
		}
		records = append(records, promptCompileRecords(
			agentID,
			runtime,
			launchID,
			sessionKind,
			freshness,
			measurement.stage,
			measurement.compiled,
			measurement.rendered,
		)...)
	}
	return records
}

func filterPromptCompileMeasurements(items []promptCompileMeasurement, keep func(promptCompileMeasurement) bool) []promptCompileMeasurement {
	filtered := items[:0]
	for _, item := range items {
		if keep(item) {
			filtered = append(filtered, item)
		}
	}
	return filtered
}

// launch creates and starts the agent process using the values resolved by
// resolve().  It attaches all standard callbacks, wires the process into the
// session, and records a checkpoint.
func (d *Dispatcher) launch(ctx context.Context, plc *processLaunchContext) (*agent.Process, error) {
	bridgeCleanup, err := d.startAgentTeamMaaSLLMBridge(ctx, plc)
	if err != nil {
		return nil, err
	}
	var bridgeCleanupOnce sync.Once
	cleanupBridge := func() {
		bridgeCleanupOnce.Do(func() {
			if bridgeCleanup != nil {
				bridgeCleanup()
			}
		})
	}
	processOwnsBridge := false
	launchCommitted := false
	defer func() {
		// A Process created by a launch that never commits is intentionally not
		// discoverable from the SessionManager shutdown snapshot. Keep its bridge
		// cleanup inside the start owner so strong shutdown cannot return while an
		// unpublished onExit callback still owns that side effect. cleanupBridge is
		// idempotent, so a racing callback remains harmless.
		if !processOwnsBridge || !launchCommitted {
			cleanupBridge()
		}
	}()

	// NewProcess starts its waiter before returning. A short-lived runtime can
	// therefore invoke onExit while launch is still installing the durable
	// delivery store and recovery callbacks. Keep exit handling behind this
	// publication barrier: on success the Process is fully registered first;
	// on failure the callback only owns lifecycle cleanup and must not start a
	// continuation for a launch that was never published.
	launchSettled := make(chan struct{})
	var launchSettledOnce sync.Once
	settleLaunch := func() {
		launchSettledOnce.Do(func() { close(launchSettled) })
	}
	defer settleLaunch()

	onExit := func(ap *agent.Process) {
		<-launchSettled
		cleanupBridge()
		if !launchCommitted {
			return
		}
		d.handleProcessExit(plc.sess, ap)
	}
	sessionMapKey := plc.sess.sessionKey
	if strings.TrimSpace(sessionMapKey) == "" {
		sessionMapKey = plc.sess.agentID
	}
	eventAgentID := firstNonEmpty(plc.binding.serverAgentID, plc.sess.serverAgentID, plc.sess.agentID)
	onEvent := d.buildEventCallback(plc.binding.channelID, sessionMapKey, eventAgentID, plc.sess.workDir, plc.sess)
	onWakeStatus := d.buildWakeStatusCallback(plc.binding.channelID, plc.binding.serverAgentID, plc.sess)
	onRuntimeEvent := d.buildRuntimeEventCallback(plc.binding.channelID, plc.binding.serverAgentID)
	launchBinary := runtimeconfig.EffectiveRuntimeBinary(plc.runtime.Type, plc.startCfg.Binary)
	resolvedBinary, err := runtimeconfig.ResolveRuntimeBinary(plc.runtime.Type, launchBinary)
	if err != nil {
		return nil, fmt.Errorf("resolve runtime binary for %q: %w", plc.runtime.Type, err)
	}
	plc.startCfg.Binary = resolvedBinary

	// Codex Spawn performs thread resume/start and may dispatch the initial turn
	// before NewProcess returns. Persist a new unsafe epoch first so a execution
	// crash anywhere inside Spawn cannot reuse the previous safe thread. A fresh
	// launch intentionally writes an empty candidate, invalidating any older id;
	// SessionInit later binds the actual fresh thread in the same epoch.
	resumeSafetyEpoch := uint64(0)
	startStatePersisted := false
	if plc.sess.persistsRuntimeState() && plc.runtime.Type == runtimeconfig.TypeCodex {
		resumeSafetyEpoch, err = d.persistStartState(plc)
		if err != nil {
			return nil, fmt.Errorf("persist pre-spawn runtime safety baseline: %w", err)
		}
		startStatePersisted = true
	}

	var process *agent.Process
	pendingScopeIdentity := messagesend.TokenIdentity(plc.agentToken)
	var initialWakeMessage *types.InboxMessage
	if isWakeMessage(plc.msg) &&
		!shouldSuppressIdleStartTurnPrompt(plc.suppressIdleStartTurn, plc.binding, plc.msg) {
		message := plc.msg
		initialWakeMessage = &message
	}
	processOptions := agent.ProcessOptions{
		WorkflowCompaction: plc.workflowCompaction,
		Wake:               plc.wake,
		InitialDelivery:    initialWakeMessage,
		OnProcessConstructed: func(process *agent.Process) {
			// Bind the freshly minted MCP bearer to this in-flight Process
			// before Spawn: first-turn tool calls that start inside Spawn
			// resolve their message-send scope here instead of through the
			// session's previous process. Released when launch returns.
			d.bindPendingScope(pendingScopeIdentity, process, plc.startReservation)
			d.bindPendingTurnTarget(process.ExecAuthorizationID(), process, plc.startReservation)
		},
		OnExit:         onExit,
		OnEvent:        onEvent,
		OnWakeStatus:   onWakeStatus,
		OnRuntimeEvent: onRuntimeEvent,
	}
	if hook, ok := overrideNewAgentProcess(); ok {
		process, err = hook(ctx, plc.driver, plc.startCfg, processOptions)
	} else {
		callbackOwner, ownerErr := d.beginProcessExitCallbackOwner()
		if ownerErr != nil {
			return nil, ownerErr
		}
		trackedOnExit := func(process *agent.Process) {
			defer d.finishProcessExitCallbackOwner(callbackOwner)
			onExit(process)
		}
		processOptions.OnExit = trackedOnExit
		process, err = agent.NewProcess(ctx, plc.driver, plc.startCfg, processOptions)
		if err != nil {
			d.finishProcessExitCallbackOwner(callbackOwner)
		}
	}
	defer d.unbindPendingScope(pendingScopeIdentity)
	if plc.startCfg.Exec != nil {
		defer d.unbindPendingTurnTarget(plc.startCfg.Exec.AuthorizationID)
	}
	if err != nil {
		return nil, err
	}
	processOwnsBridge = true
	process.SetRuntimeAttachmentResolver(d.runtimeAttachmentResolver(plc.binding))
	process.SetLaunchPromptCompileMetrics(sessionPromptCompileRecords(
		plc.agent.ID,
		string(runtimeconfig.EffectiveRuntimeType(plc.runtime.Type)),
		plc.startCfg.LaunchID,
		promptSessionKindForBinding(plc.binding),
		process.PromptSessionFreshness(),
		plc.promptCompileMetrics,
	))
	if strings.TrimSpace(plc.msg.MessageID) != "" {
		process.RememberRuntimeAckedMessage(strings.TrimSpace(plc.msg.MessageID))
		process.RememberObservedFrontier(plc.msg)
	}

	if plc.sess.persistsRuntimeState() {
		store := newSessionDeliveryOwnershipStore(plc.sess.workDir)
		var ownershipErr error
		if strings.TrimSpace(plc.ownershipHandoffMessageID) != "" {
			ownershipErr = process.SetDeliveryOwnershipStoreForHandoff(store, plc.ownershipHandoffMessageID)
		} else {
			ownershipErr = process.SetDeliveryOwnershipStore(store)
		}
		if ownershipErr != nil {
			process.Stop()
			return nil, ownershipErr
		}
		d.attachRecoveryReporter(plc.sess, process)
		// Non-Codex runtimes retain their historical post-Spawn persistence
		// semantics. Codex already installed this exact epoch before Spawn; never
		// increment it again or a fast callback replay could target the old epoch.
		if !startStatePersisted {
			resumeSafetyEpoch, err = d.persistStartState(plc)
			if err != nil {
				process.Stop()
				return nil, fmt.Errorf("persist runtime start safety baseline: %w", err)
			}
		}
		if err := d.configureResumeSafety(plc.sess, process, resumeSafetyEpoch); err != nil {
			process.Stop()
			return nil, fmt.Errorf("configure runtime resume safety: %w", err)
		}
	}
	// Spawn and all unpublished initialization remain caller-cancellable. Stop
	// relaying that cancellation before Process publication, where cancellation
	// must go through Process.Stop so onExit cannot mistake an aborted start for
	// an unexpected runtime boundary and schedule ownership continuation.
	if plc.detachStartCancellation != nil {
		if err := plc.detachStartCancellation(); err != nil {
			process.Stop()
			return nil, err
		}
	}

	// Do not publish an already-terminated Process. Its onExit callback is
	// waiting on launchSettled, so returning an error lets the caller roll back
	// the uncommitted session without racing a continuation that can later be
	// overwritten by this dead process.
	if process.State() == "stopped" {
		return nil, process.StartupFailure()
	}

	// Do not expose the Process to concurrent agent:deliver handoffs until its
	// durable ownership store is installed. Production starts additionally
	// publish through the SessionManager reservation generation: removal,
	// replacement, and an older retry are serialized with this exact commit.
	var replaced *interactiveSession
	if plc.startReservation != nil {
		var published bool
		replaced, published = d.sessionManager.PublishStart(
			plc.startReservation,
			plc.sess,
			process,
			plc.runtimeStartFingerprint,
			plc.usesWorkspaceRootMemory,
		)
		if !published {
			process.Stop()
			return nil, fmt.Errorf("runtime process start generation became stale before publication")
		}
	} else {
		// Direct helper/tests that do not enter the dispatcher session-start state
		// machine retain local publication, but no production delivery path uses
		// this branch.
		plc.sess.mu.Lock()
		plc.sess.terminalReportClaimed = false
		plc.sess.terminalReportDone = nil
		plc.sess.terminalReportWillStop = false
		plc.sess.restartRetirementInFlight = false
		plc.sess.runtimeStartReplacement = false
		plc.sess.process = process
		plc.sess.runtimeStartFingerprint = plc.runtimeStartFingerprint
		plc.sess.processUsesWorkspaceRootMemory = plc.usesWorkspaceRootMemory
		plc.sess.mu.Unlock()
	}
	if replaced != nil {
		replaced.mu.Lock()
		replacedProcess := replaced.process
		replaced.process = nil
		replaced.mu.Unlock()
		d.stopMemoryWatcherAndRetry(replaced)
		revokeSessionMCPToken(replaced)
		if replacedProcess != nil {
			d.revokeExecProcessAuthorization(replacedProcess)
			replacedProcess.Stop()
		}
	}
	d.registerRuntimeProcess(plc.sess, process)
	launchCommitted = true

	checkpointMsg := plc.checkpointMsg
	if checkpointMsg == "" {
		checkpointMsg = checkpointMsgFreshSession
	}
	if !plc.sess.suppressCheckpoints {
		process.RecordCheckpoint("start", checkpointMsg)
	}
	settleLaunch()

	return process, nil
}

func (d *Dispatcher) startAgentTeamMaaSLLMBridge(ctx context.Context, plc *processLaunchContext) (func(), error) {
	if plc == nil || plc.startCfg.Provider == nil {
		return nil, nil
	}
	serverAgentID := strings.TrimSpace(plc.binding.serverAgentID)
	if serverAgentID == "" || !providerRequiresAgentTeamMaaSLLMBridge(plc.startCfg.Provider) {
		return nil, nil
	}

	channel, ok := d.serverConnections.ServerConnection(plc.binding.channelID)
	if !ok || channel == nil {
		return nil, fmt.Errorf("agentteam llm bridge requires server connection %q", plc.binding.channelID)
	}
	issuerProvider, ok := channel.(agentteamLLMProxySessionIssuerProvider)
	if !ok {
		return nil, fmt.Errorf("server connection %q does not support agentteam llm proxy sessions", plc.binding.channelID)
	}
	issuer := issuerProvider.LLMProxySessionIssuer(serverAgentID, agentteamLLMProxyCredentialKind(plc.startCfg.Provider))
	if issuer == nil {
		return nil, fmt.Errorf("agentteam llm proxy session issuer is unavailable for %q", serverAgentID)
	}

	bridgeCtx, cancel := context.WithCancel(ctx)
	runtimeSessionID := ""
	if plc.sess != nil {
		runtimeSessionID = plc.sess.runtimeSessionID
	}

	bridge, err := agent.StartAgentTeamMaaSLLMBridge(bridgeCtx, agent.AgentTeamMaaSLLMBridgeConfig{
		UpstreamBaseURL:         plc.startCfg.Provider.BaseURL,
		Issuer:                  issuer,
		RuntimeSessionID:        runtimeSessionID,
		RuntimeSessionPublisher: d.agentteamRuntimeSessionPublisher(plc.binding.channelID, serverAgentID, runtimeSessionID),
		RuntimeDebugEnabled:     plc.startCfg.RuntimeDebugEnabled,
		AgentID:                 plc.startCfg.AgentID,
		ServerAgentID:           serverAgentID,
	})
	if err != nil {
		cancel()
		return nil, err
	}

	provider := *plc.startCfg.Provider
	provider.BaseURL = bridge.BaseURL()
	provider.APIKey = bridge.APIKey()
	provider.MetricsAPIKey = "agentteam:" + serverAgentID
	plc.startCfg.Provider = &provider
	plc.startCfg.Env = runtimeconfig.RuntimeEnvOverridesWithLoopbackURLs(plc.startCfg.Env, provider.BaseURL)

	return func() {
		cancel()
		_ = bridge.Close()
	}, nil
}

func shouldAllocateAgentTeamRuntimeSession(binding conversationBinding) bool {
	return strings.TrimSpace(binding.serverAgentID) != ""
}

func newAgentTeamRuntimeSessionID() (string, error) {
	var buf [24]byte
	if _, err := rand.Read(buf[:]); err != nil {
		return "", fmt.Errorf("generate agentteam runtime session id: %w", err)
	}
	return "ars_" + hex.EncodeToString(buf[:]), nil
}

func (d *Dispatcher) agentteamRuntimeSessionPublisher(channelID string, serverAgentID string, runtimeSessionID string) func(context.Context, string) error {
	trimmedChannelID := strings.TrimSpace(channelID)
	trimmedAgentID := strings.TrimSpace(serverAgentID)
	trimmedSessionID := strings.TrimSpace(runtimeSessionID)
	if trimmedChannelID == "" || trimmedAgentID == "" || trimmedSessionID == "" {
		return nil
	}
	return func(_ context.Context, runtimeSessionID string) error {
		publishedSessionID := strings.TrimSpace(runtimeSessionID)
		if publishedSessionID == "" {
			publishedSessionID = trimmedSessionID
		}
		reporter := d.eventReporter(trimmedChannelID)
		if reporter == nil {
			return fmt.Errorf("agentteam runtime session publisher is unavailable for server connection %q", trimmedChannelID)
		}
		reporter.Emit(types.SessionEvent{
			AgentID:   trimmedAgentID,
			SessionID: publishedSessionID,
		})
		return nil
	}
}

func providerRequiresAgentTeamMaaSLLMBridge(prov *drivercontract.ProviderConfig) bool {
	if prov == nil {
		return false
	}
	return agentteamLLMProxyCredentialKind(prov) == agent.LLMCredentialKindMaaSCapability
}

func agentteamLLMProxyCredentialKind(prov *drivercontract.ProviderConfig) string {
	if prov == nil {
		return ""
	}
	switch strings.TrimSpace(prov.CredentialKind) {
	case agent.LLMCredentialKindMaaSCapability:
		return agent.LLMCredentialKindMaaSCapability
	default:
		return ""
	}
}

// resolveInstructionContext resolves AgentTeam skill documentation and the memory
// resume reminder from the MCP config list.
// This is the single authoritative implementation — previously duplicated in
// three places.
func (d *Dispatcher) resolveInstructionContext(_ string, mcps []types.NamedMCPConfig, rt runtimeconfig.RuntimeType) resolvedInstructionContext {
	var result resolvedInstructionContext

	for _, mcpConfig := range mcps {
		t := toolNamePrefix(rt, mcpConfig.Name)
		if mcpConfig.Name == "agentteam" {
			result.skillPlan = promptcompiler.CompileAgentTeamSkillPlan(t, t("memory_search"))
			result.skillDocParts = types.SkillDocParts{}
			result.memoryResumeReminder = promptcompiler.AgentTeamMemoryResumeReminder()
			continue
		}
	}
	return result
}

// resolveModelAndReasoning returns the effective model and reasoning effort for
// a deferred continuation or context recovery, preferring saved state values
// over agent config defaults.
func resolveModelAndReasoning(state *sessionResumeState, ag agent.Config) (model, reasoningEffort string) {
	if state.LastStartConfig != nil {
		model = strings.TrimSpace(state.LastStartConfig.Model)
		reasoningEffort = strings.TrimSpace(state.LastStartConfig.ReasoningEffort)
	}
	if model == "" {
		model = strings.TrimSpace(ag.Model)
	}
	if reasoningEffort == "" {
		reasoningEffort = strings.TrimSpace(ag.ReasoningEffort)
	}
	return model, reasoningEffort
}

// buildWakeFromDeferredMessage creates a WakeInfo from a parsed deferred
// inbox message.
func buildWakeFromDeferredMessage(msg types.InboxMessage) *agent.WakeInfo {
	return &agent.WakeInfo{
		MessageID:     strings.TrimSpace(msg.MessageID),
		Target:        strings.TrimSpace(msg.Target),
		TargetKey:     strings.TrimSpace(msg.TargetKey),
		TargetSeq:     msg.TargetSeq,
		MessageType:   strings.TrimSpace(msg.MessageType),
		DeliveryClass: agent.DeliveryClassForMessage(msg),
	}
}

func newPromptMetricsLaunchID() string {
	return "launch-" + strings.ToLower(uuid.NewString())
}
