Add a useDebouncedSave hook and delete Settings.stream
Two items from Stage 2 of `plan/17-refactor.md`. `frontend/src/hooks/useDebouncedSave.js` replaces three copies of the same debounce. `PlotPanel` and `ScenarioEditor` held identical per-key timer maps. `ScriptEditor` held a single shared timer, so editing two fields inside the same 600 ms window canceled the first save. The hook gives every key its own timer, which fixes that. `Settings.stream` was dead state. Nothing read it and every turn streams. This removes the column, both schema fields, and adds migration 65 to drop it. It is item S1 in `docs/self-review.md`. Migration 65 needs a new guard. `_column_already_gone` is the counterpart to `_column_already_there`: `create_all` builds the current schema, which is already missing every dropped column, so a fixture that stamps an old version and replays would fail on a column that is not there. Verified: 549 backend tests pass, lint and build are clean, and migration 65 runs both ways, once against a database that still has the column and once against one that does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Dix4oGV3njgWRdu7P9t6r
This commit is contained in:
co-authored by
Claude Opus 5
parent
2c57b1ceab
commit
e0bf2b61d9
@@ -297,6 +297,10 @@ MIGRATIONS: list[tuple[int, str | dict[str, str]]] = [
|
||||
# under.
|
||||
(63, "ALTER TABLE actions ADD COLUMN parent_id INTEGER REFERENCES actions(id) ON DELETE SET NULL"),
|
||||
(64, "CREATE INDEX IF NOT EXISTS ix_actions_parent ON actions (parent_id)"),
|
||||
# Phase 17: `Settings.stream` was dead state. Nothing ever read it, and every
|
||||
# turn streams. This is item S1 in `docs/self-review.md`. The table holds one
|
||||
# row per user, so the rewrite is small and needs no VACUUM FULL.
|
||||
(65, "ALTER TABLE settings DROP COLUMN stream"),
|
||||
]
|
||||
|
||||
LATEST_VERSION = max((v for v, _ in MIGRATIONS), default=1)
|
||||
@@ -427,6 +431,25 @@ def _for_dialect(sql: str | dict[str, str], dialect: str) -> str:
|
||||
# Matches the ADD COLUMN migrations in this file. Every one is written above,
|
||||
# so this pattern parses only SQL this file controls.
|
||||
_ADD_COLUMN = re.compile(r"^\s*ALTER\s+TABLE\s+(\w+)\s+ADD\s+COLUMN\s+\"?(\w+)\"?", re.I)
|
||||
_DROP_COLUMN = re.compile(r"^\s*ALTER\s+TABLE\s+(\w+)\s+DROP\s+COLUMN\s+\"?(\w+)\"?", re.I)
|
||||
|
||||
|
||||
def _column_already_gone(conn, sql: str) -> bool:
|
||||
"""Returns `True` when `sql` drops a column the table no longer has.
|
||||
|
||||
This is the counterpart to `_column_already_there`, for the same reason.
|
||||
`create_all` builds the current schema, which is already missing every
|
||||
column a migration drops. Replaying from an older stamp against a database
|
||||
built that way would fail on a column that is not there.
|
||||
"""
|
||||
match = _DROP_COLUMN.match(sql)
|
||||
if match is None:
|
||||
return False
|
||||
table, column = match.group(1), match.group(2)
|
||||
inspector = inspect(conn)
|
||||
if table not in inspector.get_table_names():
|
||||
return False
|
||||
return column not in {col["name"] for col in inspector.get_columns(table)}
|
||||
|
||||
|
||||
def _column_already_there(conn, sql: str) -> bool:
|
||||
@@ -946,7 +969,8 @@ def bootstrap(engine: Engine) -> None:
|
||||
statement = _for_dialect(sql, conn.dialect.name)
|
||||
# Skip the DDL when it has already run. The data pass below it
|
||||
# still runs.
|
||||
if not _column_already_there(conn, statement):
|
||||
if not (_column_already_there(conn, statement)
|
||||
or _column_already_gone(conn, statement)):
|
||||
conn.execute(text(statement))
|
||||
if version == WORLD_DELTA_VERSION:
|
||||
_backfill_world_delta(conn)
|
||||
|
||||
@@ -619,7 +619,6 @@ class Settings(Base):
|
||||
"for the player's next action."
|
||||
),
|
||||
)
|
||||
stream: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
# Phase 6: auto-summarization + memory bank
|
||||
summary_model: Mapped[str] = mapped_column(String(200), default="") # "" = main model
|
||||
embedding_model: Mapped[str] = mapped_column(String(200), default="") # "" = bank disabled
|
||||
|
||||
@@ -449,7 +449,6 @@ class SettingsOut(ORMModel):
|
||||
reasoning_max_tokens: int
|
||||
context_token_budget: int
|
||||
narrator_prompt: str
|
||||
stream: bool
|
||||
summary_model: str
|
||||
embedding_model: str
|
||||
memory_bank_capacity: int
|
||||
@@ -496,7 +495,6 @@ class SettingsUpdate(BaseModel):
|
||||
reasoning_max_tokens: Annotated[int, Field(ge=-1, le=100_000)] | None = None
|
||||
context_token_budget: Annotated[int, Field(ge=256, le=200_000)] | None = None
|
||||
narrator_prompt: Prose | None = None
|
||||
stream: bool | None = None
|
||||
summary_model: Name | None = None
|
||||
embedding_model: Name | None = None
|
||||
memory_bank_capacity: Annotated[int, Field(ge=1, le=1000)] | None = None
|
||||
|
||||
Reference in New Issue
Block a user