| 1 | package bot |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "strings" |
| 7 | |
| 8 | "reasonix/internal/boot" |
| 9 | "reasonix/internal/config" |
| 10 | "reasonix/internal/control" |
| 11 | "reasonix/internal/event" |
| 12 | "reasonix/internal/secrets" |
| 13 | ) |
| 14 | |
| 15 | // The session queue owns this new turn. Approval replies and child work never |
| 16 | // enter here. Keep the previous state and lease until the complete replacement |
| 17 | // is ready, then fence publication against retirement and a newer disk save. |
| 18 | func (gw *BotGateway) applySessionModelSettings(ctx context.Context, key string, msg InboundMessage, previous *sessionState) (*sessionState, error) { |
| 19 | old, ok := previous.ctrl.(*control.Controller) |
| 20 | if !ok { |
| 21 | return previous, nil |
| 22 | } |
| 23 | previous.lifecycleMu.Lock() |
| 24 | defer previous.lifecycleMu.Unlock() |
| 25 | for { |
| 26 | if err := ctx.Err(); err != nil { |
| 27 | return nil, err |
| 28 | } |
| 29 | if previous.retired { |
| 30 | return nil, errBotSessionRetired |
| 31 | } |
| 32 | applied, desired, err := old.ModelSettingsState() |
| 33 | if err != nil { |
| 34 | return nil, err |
| 35 | } |
| 36 | if applied == desired { |
| 37 | return previous, nil |
| 38 | } |
| 39 | if botSessionHasActiveWork(previous) { |
| 40 | return nil, fmt.Errorf("saved model settings are pending until current work finishes") |
| 41 | } |
| 42 | cfg, err := config.LoadModelRuntimeSnapshot(previous.workspaceRoot, old.ModelRef()) |
| 43 | if err != nil { |
| 44 | return nil, err |
| 45 | } |
| 46 | model := old.ModelRef() |
| 47 | if entry, ok := cfg.ResolveModel(model); !ok || !entry.Configured() { |
| 48 | var available bool |
| 49 | model, _, available = cfg.ResolveNewSessionChatModel() |
| 50 | if !available || strings.TrimSpace(model) == "" { |
| 51 | return nil, fmt.Errorf("choose a configured model before starting another bot request") |
| 52 | } |
| 53 | } |
| 54 | next := &sessionState{ |
| 55 | sink: &sessionEventSink{}, leases: previous.leases, |
| 56 | platform: previous.platform, connectionID: previous.connectionID, |
| 57 | model: previous.model, workspaceRoot: previous.workspaceRoot, |
| 58 | toolApprovalMode: previous.toolApprovalMode, sessionPath: previous.sessionPath, |
| 59 | mappingDegraded: previous.mappingDegraded, createdAt: previous.createdAt, lastActive: previous.lastActive, |
| 60 | pendingApprovals: map[string]event.Approval{}, pendingAsks: map[string][]event.AskQuestion{}, |
| 61 | } |
| 62 | next.onSessionTransition = gw.botSessionTransitionHandler(key, msg, next) |
| 63 | result, err := boot.Rebuild(ctx, old, boot.Options{ |
| 64 | Model: model, RequireKey: true, RuntimeReload: boot.RuntimeReload{ForceFullRebuild: true}, |
| 65 | MaxSteps: gw.cfg.MaxSteps, MaxStepsKey: "bot.max_steps", Sink: next.sink, |
| 66 | StatsSource: "bot", WorkspaceRoot: next.workspaceRoot, SessionDir: botSessionDir(next.workspaceRoot), |
| 67 | ApprovalTimeout: gw.approvalTimeout(), |
| 68 | OnSessionRecovered: gw.botSessionRecoveredHandler(key, msg, next), OnSessionTransition: next.onSessionTransition, |
| 69 | }) |
| 70 | if err != nil { |
| 71 | return nil, err |
| 72 | } |
| 73 | next.ctrl = result.Controller |
| 74 | result.Controller.EnableInteractiveApproval() |
| 75 | a, d, err := result.Controller.ModelSettingsState() |
| 76 | if err != nil || a != d { |
| 77 | result.Controller.Close() |
| 78 | if err != nil { |
| 79 | return nil, err |
| 80 | } |
| 81 | continue |
| 82 | } |
| 83 | if err := bindBotSessionWriteAuthority(next); err != nil { |
| 84 | result.Controller.Close() |
| 85 | _ = bindBotSessionWriteAuthority(previous) |
| 86 | return nil, err |
| 87 | } |
| 88 | gw.mu.Lock() |
| 89 | if gw.controllers[key] != previous { |
| 90 | gw.mu.Unlock() |
| 91 | result.Controller.Close() |
| 92 | _ = bindBotSessionWriteAuthority(previous) |
| 93 | return nil, fmt.Errorf("bot session changed while applying saved model settings") |
| 94 | } |
| 95 | if err := control.ActivateControllerReplacement(old, result.Controller); err != nil { |
| 96 | gw.mu.Unlock() |
| 97 | result.Controller.ReleaseResources() |
| 98 | _ = bindBotSessionWriteAuthority(previous) |
| 99 | return nil, fmt.Errorf("activate replacement bot runtime: %w", err) |
| 100 | } |
| 101 | gw.controllers[key] = next |
| 102 | previous.leases = nil |
| 103 | previous.retired = true |
| 104 | gw.mu.Unlock() |
| 105 | result.Controller.ActivateGoalDriverAfterRebuild() |
| 106 | old.Close() |
| 107 | return next, nil |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | func (gw *BotGateway) sessionForNewTurn(ctx context.Context, adapter Adapter, key string, msg InboundMessage) *sessionState { |
| 112 | // 获取或创建 Controller |
| 113 | state := gw.getOrCreateSession(ctx, key, msg) |
| 114 | if state == nil || state.ctrl == nil { |
| 115 | _ = gw.sendText(ctx, adapter, msg, "内部错误:无法创建会话。") |
| 116 | return nil |
| 117 | } |
| 118 | var settingsErr error |
| 119 | state, settingsErr = gw.applySessionModelSettings(ctx, key, msg, state) |
| 120 | if settingsErr != nil { |
| 121 | gw.logger.Warn("bot model settings application failed", "err", secrets.RedactError(settingsErr)) |
| 122 | _ = gw.sendText(ctx, adapter, msg, "模型设置已保存,但当前会话尚未成功应用。请检查可用模型后重试;原会话仍保留。") |
| 123 | return nil |
| 124 | } |
| 125 | return state |
| 126 | } |
| 127 |