Skip to content

fix(lifecycle): Next-2.0.1 hotfix release#58

Merged
NickCharlie merged 7 commits intomainfrom
develop
Feb 24, 2026
Merged

fix(lifecycle): Next-2.0.1 hotfix release#58
NickCharlie merged 7 commits intomainfrom
develop

Conversation

@NickCharlie
Copy link
Owner

@NickCharlie NickCharlie commented Feb 24, 2026

概述

Next-2.0.1 热修复版本,修复插件卸载/重载卡死、MySQL 兼容性及人格审批流程问题。

Summary

Hotfix release for Next-2.0.1, resolving plugin uninstall/reload hang, MySQL compatibility, and persona approval issues.

修复内容

插件生命周期

  • 修复卸载/重载时 CPU 占用过高 后台任务在关停时正确取消
  • 所有关停步骤添加超时保护,防止单服务阻塞整个流程
  • Server.stop() 使用线程池执行器避免阻塞事件循环
  • 关停时清理 SingletonABCMeta._instances 防止重载后单例残留

MySQL 兼容性

  • 修复 persona_content 列 NOT NULL 约束导致 INSERT 失败
  • 修复 TEXT 列 DEFAULT 值导致严格模式报错
  • 启用自动列迁移,跳过 TEXT/BLOB/JSON 列的 DEFAULT 生成
  • Facade 65 处延迟导入移至模块级别,修复热重载后 ModuleNotFoundError

人格审批

  • 传统审批路径改为通过 PersonaWebManager 路由,修复跨线程调用卡死
  • 修复 save_or_update_jargon 参数顺序和类型错误

测试计划

  • 插件安装/卸载/重载正常
  • MySQL 环境下人格备份正常写入
  • 人格审批后自动应用
  • WebUI 正常加载

Summary by Sourcery

Stabilize plugin lifecycle shutdown and improve MySQL compatibility and persona/jargon data handling for the Next-2.0.1 hotfix release.

Bug Fixes:

  • Prevent plugin unload/reload hangs by tracking background asyncio tasks and cancelling them with per-step timeouts during shutdown.
  • Avoid event loop blocking when stopping the WebUI server by moving thread joins into a thread pool and adding a lock acquisition timeout.
  • Fix MySQL strict-mode issues by avoiding DEFAULT values on TEXT-like columns and ensuring persona backups always persist non-null persona_content.
  • Resolve potential errors in persona and jargon flows by ensuring persona_content is stored on backup and centralizing ORM imports to avoid hot-reload import failures.

Enhancements:

  • Add timeouts and error isolation to service and learning-task shutdown paths to prevent single services from blocking overall shutdown.
  • Introduce graceful cancellation and logging for long-running periodic tasks in intelligence, interaction, and affection services.
  • Enable automatic column migration on startup to keep the database schema in sync without manual intervention.

Documentation:

  • Document the Next-2.0.1 hotfix changes in the changelog, including lifecycle, MySQL, and persona-related fixes.

Chores:

  • Bump the plugin metadata version to Next-2.0.1.

MySQL strict mode prohibits default values on TEXT/BLOB columns.
Remove server_default from persona_content in PersonaBackup ORM.
Enable column-level auto-migration by default so missing columns
(e.g. user_id on composite_psychological_states) are added via
ALTER TABLE on existing databases.

Skip DEFAULT clause for TEXT/BLOB/JSON columns in ALTER statements
to avoid MySQL strict mode errors.
Lazy relative imports (from ....models) inside method bodies fail
with ModuleNotFoundError after plugin hot-reload due to corrupted
module hierarchy. Move all 65 lazy imports to module top level
across 10 facade files.
Wrap every shutdown step with asyncio.wait_for to prevent the plugin
from hanging on uninstall. Each service stop gets a 5s timeout, each
lifecycle step gets 8s, and background task cancellation gets 3s per
task. Server.stop() now runs thread.join in an executor to avoid
blocking the event loop.
Root cause of 100% CPU during uninstall/reload: five background tasks
with `while True` loops were created via asyncio.create_task() but
never tracked or cancelled during shutdown. They kept running after
the plugin was unloaded, spinning the event loop.

- affection_manager: track _mood_task, cancel in _do_stop
- enhanced_interaction: track _sync_task/_cleanup_task, cancel in _do_stop
- intelligence_enhancement: track _knowledge_task/_recommend_task, cancel
- plugin_lifecycle: register all create_task calls in background_tasks
- plugin_lifecycle: clear SingletonABCMeta._instances on shutdown
The persona_backups table has persona_content as NOT NULL in existing
MySQL databases. The INSERT was passing None because the field was
never set, causing IntegrityError 1048.
Remove VIDEO_SCRIPT.md, update CHANGELOG with all fixes since 2.0.0
@sourcery-ai
Copy link

sourcery-ai bot commented Feb 24, 2026

Reviewer's Guide

Hotfix release Next-2.0.1 focuses on making plugin/service shutdown robust (tracked background tasks, per-step timeouts, non-blocking WebUI stop), improving MySQL compatibility (TEXT/BLOB default handling, persona backup content), and fixing persona/jargon approval paths and SQLAlchemy facade imports for better hot-reload behavior.

Sequence diagram for plugin shutdown and background task cancellation

sequenceDiagram
    actor Admin
    participant PluginLifecycle as PluginLifecycle
    participant PluginInstance as PluginInstance_p
    participant GroupOrchestrator as GroupLearningOrchestrator
    participant LearningScheduler as LearningScheduler
    participant BackgroundTasks as background_tasks_set
    participant FactoryManager as FactoryManager
    participant V2Integration as V2LearningIntegration
    participant TempPersonaUpdater as TemporaryPersonaUpdater
    participant MessageCollector as MessageCollector
    participant WebUIManager as WebUIManager

    Admin->>PluginLifecycle: shutdown()
    activate PluginLifecycle
    PluginLifecycle->>PluginInstance: get _group_orchestrator
    alt has_group_orchestrator
        PluginLifecycle->>GroupOrchestrator: cancel_all() via _safe_step
        activate GroupOrchestrator
        GroupOrchestrator->>GroupOrchestrator: stop_learning() with timeout 3s
        GroupOrchestrator->>GroupOrchestrator: cancel learning_tasks with per-task timeout 2s
        deactivate GroupOrchestrator
    end

    alt has_learning_scheduler
        PluginLifecycle->>LearningScheduler: stop() via _safe_step
    end

    PluginLifecycle->>BackgroundTasks: iterate over tasks
    loop for_each_task
        PluginLifecycle->>BackgroundTasks: task.cancel() if not done
        PluginLifecycle->>BackgroundTasks: wait_for(shield(task), timeout=3s)
    end
    PluginLifecycle->>BackgroundTasks: clear()

    alt has_factory_manager
        PluginLifecycle->>FactoryManager: cleanup() via _safe_step
    end

    alt has_v2_integration
        PluginLifecycle->>V2Integration: stop() via _safe_step
    end

    PluginLifecycle->>PluginLifecycle: clear SingletonABCMeta._instances

    alt has_temporary_persona_updater
        PluginLifecycle->>TempPersonaUpdater: cleanup_temp_personas() via _safe_step
    end

    alt has_message_collector
        PluginLifecycle->>MessageCollector: save_state() via _safe_step
    end

    alt has_webui_manager
        PluginLifecycle->>WebUIManager: stop() via _safe_step
    end

    PluginLifecycle-->>Admin: shutdown_complete
    deactivate PluginLifecycle
Loading

Sequence diagram for WebUI stop with lock timeout and non-blocking server thread join

sequenceDiagram
    participant PluginLifecycle as PluginLifecycle
    participant WebUIManager as WebUIManager
    participant CleanupLock as _server_cleanup_lock
    participant WebUIServer as WebUIServer
    participant ServerThread as server_thread
    participant EventLoop as asyncio_event_loop

    PluginLifecycle->>WebUIManager: stop()
    activate WebUIManager
    WebUIManager->>CleanupLock: acquire(timeout=3s)
    alt lock_acquired
        WebUIManager->>WebUIServer: stop()
        alt server_has_thread
            WebUIServer->>EventLoop: run_in_executor(join_thread, 5s)
            activate EventLoop
            EventLoop->>ServerThread: join(timeout=5s)
            alt join_completes_in_6s
                ServerThread-->>EventLoop: joined
                EventLoop-->>WebUIServer: ok
            else join_timeout
                EventLoop-->>WebUIServer: asyncio.TimeoutError
                WebUIServer->>WebUIServer: log timeout warning
            end
            deactivate EventLoop
            WebUIServer->>WebUIServer: set server_thread=None
        else no_thread
            WebUIServer->>WebUIServer: skip_thread_join
        end
        WebUIManager->>CleanupLock: release()
        WebUIManager-->>PluginLifecycle: stop_completed
    else lock_timeout
        WebUIManager->>WebUIManager: log lock timeout warning
        alt server_instance_exists
            WebUIManager->>WebUIServer: stop() best_effort
        end
        WebUIManager->>WebUIManager: set _server_instance=None
        WebUIManager-->>PluginLifecycle: stop_completed_without_lock
    end
    deactivate WebUIManager
Loading

Updated class diagram for background services, service registry, and PersonaBackup

classDiagram
    class ServiceRegistry{
        +Dict~str,Any~ _services
        +int _SERVICE_STOP_TIMEOUT
        +bool start_all_services()
        +bool stop_all_services()
        +Dict~str,str~ get_service_status()
    }

    class PluginLifecycle{
        -Any _plugin
        -Any _webui_manager
        -float _STEP_TIMEOUT
        -float _TASK_CANCEL_TIMEOUT
        +async shutdown() None
        +async _safe_step(label str, coro Any, timeout float) None
    }

    class IntelligenceEnhancementService{
        -Any _logger
        -Task _knowledge_task
        -Task _recommend_task
        +async _do_start() bool
        +async _do_stop() bool
        +async _periodic_knowledge_update() None
        +async _periodic_recommendation_refresh() None
    }

    class EnhancedInteractionService{
        -Any _logger
        -Task _sync_task
        -Task _cleanup_task
        -float memory_sync_interval
        -float context_retention_time
        +async _do_start() bool
        +async _do_stop() bool
        +async _periodic_memory_sync() None
        +async _periodic_context_cleanup() None
    }

    class AffectionManager{
        -Any _logger
        -Any config
        -Task _mood_task
        +async _do_start() bool
        +async _do_stop() bool
        +async _daily_mood_updater() None
    }

    class DatabaseEngine{
        +Any engine
        +async create_tables(enable_auto_migration bool) None
        -_get_existing_columns(sync_conn Any) Dict~str,Any~
    }

    class SQLAlchemyDatabaseManager{
        +Any engine
        +async start() bool
        -_init_facades() None
    }

    class PersonaBackup{
        +Integer id
        +String persona_id
        +Text persona_name
        +Text persona_description
        +Text persona_traits
        +Text persona_memory
        +Text persona_goals
        +Text persona_constraints
        +Text imitation_dialogues
        +Text backup_reason
        +Float backup_time
        +Text persona_content
        +DateTime created_at
    }

    class PersonaFacade{
        +async backup_persona(backup_data Dict~str,Any~) bool
        +async get_persona_update_history(limit int) List~Dict~str,Any~~
    }

    class WebUIServer{
        -Thread server_thread
        -Any _thread_loop
        +async stop() None
    }

    class WebUIManager{
        +async stop() None
    }

    ServiceRegistry --> PluginLifecycle : manages
    PluginLifecycle --> WebUIManager : uses
    WebUIManager --> WebUIServer : controls

    SQLAlchemyDatabaseManager --> DatabaseEngine : owns
    PersonaFacade --> PersonaBackup : creates

    IntelligenceEnhancementService ..> ServiceRegistry : registered_as_service
    EnhancedInteractionService ..> ServiceRegistry : registered_as_service
    AffectionManager ..> ServiceRegistry : registered_as_service

    IntelligenceEnhancementService --> PluginLifecycle : background_tasks_cancelled_on_shutdown
    EnhancedInteractionService --> PluginLifecycle : background_tasks_cancelled_on_shutdown
    AffectionManager --> PluginLifecycle : background_tasks_cancelled_on_shutdown
Loading

File-Level Changes

Change Details Files
Make plugin lifecycle and services shutdown bounded-time and properly cancel background tasks to avoid hangs and high CPU during uninstall/reload.
  • Track all created background tasks in a per-plugin background_tasks set and register done callbacks to auto-remove them
  • Introduce a _safe_step helper and per-step timeouts in plugin shutdown, including per-task cancel timeouts, and add timeouts around stopping group learning and other services
  • Ensure WebUI stop acquires the cleanup lock with timeout and continues cleanup even on lock acquisition failure, and make Server.stop wait for the WebUI thread via run_in_executor with a timeout
  • Add stop_all_services per-service timeout handling and clear SingletonABCMeta._instances during shutdown to avoid stale singletons
  • Teach long-running periodic services (intelligence_enhancement, enhanced_interaction, affection_manager) to keep task references, cancel them in _do_stop, and handle asyncio.CancelledError in loops
core/plugin_lifecycle.py
core/patterns.py
webui/manager.py
webui/server.py
services/analysis/intelligence_enhancement.py
services/state/enhanced_interaction.py
services/state/affection_manager.py
services/learning/group_orchestrator.py
Improve MySQL compatibility and schema migration behavior, especially around TEXT columns and persona backups.
  • Skip generating DEFAULT clauses for TEXT/BLOB/JSON columns when auto-migrating tables so MySQL strict mode is not violated
  • Enable auto-migration in SQLAlchemyDatabaseManager.start when creating tables
  • Adjust PersonaBackup.persona_content column to remove default/server_default and ensure backup_persona always writes a non-null persona_content value
core/database/engine.py
services/database/sqlalchemy_database_manager.py
models/orm/psychological.py
services/database/facades/persona_facade.py
Stabilize persona approval and jargon/social/expression/metrics/admin/message facades by fixing argument usage and moving imports to module scope for hot-reload safety.
  • Move numerous in-function SQLAlchemy and ORM imports in facades to module level to avoid ModuleNotFoundError on hot reload and reduce per-call overhead
  • Ensure save_or_update_jargon uses the correct parameter order/types and centralize ORM imports for Jargon operations
  • Centralize ORM and SQL imports in learning, metrics, expression, social, admin, message, psychological, and reinforcement facades to share models and queries consistently
services/database/facades/learning_facade.py
services/database/facades/jargon_facade.py
services/database/facades/social_facade.py
services/database/facades/metrics_facade.py
services/database/facades/expression_facade.py
services/database/facades/admin_facade.py
services/database/facades/message_facade.py
services/database/facades/persona_facade.py
services/database/facades/reinforcement_facade.py
services/database/facades/psychological_facade.py
Versioning and documentation updates for the Next-2.0.1 hotfix release.
  • Document the Next-2.0.1 hotfix in CHANGELOG with details on lifecycle, MySQL, and persona fixes
  • Bump plugin version metadata from Next-2.0.0 to Next-2.0.1
  • Remove VIDEO_SCRIPT.md from the repository
CHANGELOG.md
metadata.yaml
VIDEO_SCRIPT.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The periodic background loops that previously caught and logged generic exceptions now only handle asyncio.CancelledError, so any unexpected error will terminate the task permanently; consider keeping an internal try/except (with logging) inside the loop to avoid silent degradation over time.
  • The various shutdown timeouts (_STEP_TIMEOUT, _TASK_CANCEL_TIMEOUT, _SERVICE_STOP_TIMEOUT, WebUI lock/stop timeouts, etc.) are currently hardcoded; consider centralizing or making them configurable so they can be tuned for different environments without code changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The periodic background loops that previously caught and logged generic exceptions now only handle `asyncio.CancelledError`, so any unexpected error will terminate the task permanently; consider keeping an internal try/except (with logging) inside the loop to avoid silent degradation over time.
- The various shutdown timeouts (`_STEP_TIMEOUT`, `_TASK_CANCEL_TIMEOUT`, `_SERVICE_STOP_TIMEOUT`, WebUI lock/stop timeouts, etc.) are currently hardcoded; consider centralizing or making them configurable so they can be tuned for different environments without code changes.

## Individual Comments

### Comment 1
<location path="core/database/engine.py" line_range="290-292" />
<code_context>
-                                default = f" DEFAULT {col.server_default.arg!r}"
-                            elif col.default is not None and col.default.is_scalar:
-                                default = f" DEFAULT {col.default.arg!r}"
+                            # MySQL 不允许 TEXT/BLOB 列有 DEFAULT 值
+                            is_text_type = col_type.upper() in ("TEXT", "BLOB", "MEDIUMTEXT", "LONGTEXT", "JSON")
+                            if not is_text_type:
+                                if col.server_default is not None:
+                                    default = f" DEFAULT {col.server_default.arg!r}"
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Type comparison for TEXT/BLOB defaults may miss dialect-specific variants, causing unexpected defaults to be emitted.

`is_text_type` depends on `col_type.upper()` matching one of a few exact strings. Dialects/configs may produce variants like `TINYTEXT`, `LONGBLOB`, or `TEXT CHARACTER SET utf8mb4`, which would bypass this check and allow invalid defaults again. To harden this, consider prefix/suffix checks (e.g. `startswith("TEXT")` / `endswith("BLOB")`) or, preferably, use SQLAlchemy’s type classes instead of string comparisons.

Suggested implementation:

```python
                            col_type = col.type.compile(self.engine.dialect)
                            nullable = "NULL" if col.nullable else "NOT NULL"
                            default = ""

                            # MySQL 不允许 TEXT/BLOB/JSON 列有 DEFAULT 值
                            # 使用 SQLAlchemy 类型而不是字符串比较,以兼容不同方言生成的类型名称
                            from sqlalchemy import Text, LargeBinary
                            from sqlalchemy.dialects.mysql import JSON as MySQLJSON

                            is_text_blob_or_json_type = isinstance(
                                col.type,
                                (Text, LargeBinary, MySQLJSON),
                            )

                            if not is_text_blob_or_json_type:
                                if col.server_default is not None:
                                    default = f" DEFAULT {col.server_default.arg!r}"
                                elif col.default is not None and col.default.is_scalar:
                                    default = f" DEFAULT {col.default.arg!r}"
                            alter_statements.append(

```

To follow best practices and avoid importing inside the function/method body, you should:

1. Move the `from sqlalchemy import Text, LargeBinary` and `from sqlalchemy.dialects.mysql import JSON as MySQLJSON` imports to the top of `core/database/engine.py`, alongside the other imports.
2. After moving the imports, remove the two import lines from this block, leaving only the `is_text_blob_or_json_type = isinstance(...)` check.

If `core/database/engine.py` is intended to be dialect-agnostic and used with non-MySQL engines, you may additionally want to guard this logic with a dialect check (e.g. only apply the `is_text_blob_or_json_type` restriction when `self.engine.dialect.name == "mysql"` or `"mariadb"`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +290 to +292
# MySQL 不允许 TEXT/BLOB 列有 DEFAULT 值
is_text_type = col_type.upper() in ("TEXT", "BLOB", "MEDIUMTEXT", "LONGTEXT", "JSON")
if not is_text_type:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Type comparison for TEXT/BLOB defaults may miss dialect-specific variants, causing unexpected defaults to be emitted.

is_text_type depends on col_type.upper() matching one of a few exact strings. Dialects/configs may produce variants like TINYTEXT, LONGBLOB, or TEXT CHARACTER SET utf8mb4, which would bypass this check and allow invalid defaults again. To harden this, consider prefix/suffix checks (e.g. startswith("TEXT") / endswith("BLOB")) or, preferably, use SQLAlchemy’s type classes instead of string comparisons.

Suggested implementation:

                            col_type = col.type.compile(self.engine.dialect)
                            nullable = "NULL" if col.nullable else "NOT NULL"
                            default = ""

                            # MySQL 不允许 TEXT/BLOB/JSON 列有 DEFAULT 值
                            # 使用 SQLAlchemy 类型而不是字符串比较,以兼容不同方言生成的类型名称
                            from sqlalchemy import Text, LargeBinary
                            from sqlalchemy.dialects.mysql import JSON as MySQLJSON

                            is_text_blob_or_json_type = isinstance(
                                col.type,
                                (Text, LargeBinary, MySQLJSON),
                            )

                            if not is_text_blob_or_json_type:
                                if col.server_default is not None:
                                    default = f" DEFAULT {col.server_default.arg!r}"
                                elif col.default is not None and col.default.is_scalar:
                                    default = f" DEFAULT {col.default.arg!r}"
                            alter_statements.append(

To follow best practices and avoid importing inside the function/method body, you should:

  1. Move the from sqlalchemy import Text, LargeBinary and from sqlalchemy.dialects.mysql import JSON as MySQLJSON imports to the top of core/database/engine.py, alongside the other imports.
  2. After moving the imports, remove the two import lines from this block, leaving only the is_text_blob_or_json_type = isinstance(...) check.

If core/database/engine.py is intended to be dialect-agnostic and used with non-MySQL engines, you may additionally want to guard this logic with a dialect check (e.g. only apply the is_text_blob_or_json_type restriction when self.engine.dialect.name == "mysql" or "mariadb").

@NickCharlie NickCharlie merged commit 4926171 into main Feb 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant