# Session handoff — 2026-06-22 (77) Continues from handoff-76. Per the user's direction (handoff-76 §7: clear the open Gitea issues before resuming feature work), this session took the **bug** of the four open issues — **#39** (Form-B insert misparse against a stale schema cache after fast DDL) — and fixed it end-to-end. Three open issues remain (#36/#37/#38, all enhancements); see §3. ## §1. State **Branch `main`.** Commits this session (on `main`, **not pushed** — push is the user's step): - `07575da` `fix(app)` — schema-refresh submission gate (#39) + Tier-1 and Tier-4 PTY regression tests + ADR-0022 Amendment 8 + README index. - this `docs(handoff-77)` commit — folds in the `CHANGELOG.md` `[Unreleased]` Fixed bullet for #39 **and** a new **changelog-discipline rule** in project `CLAUDE.md` (see §4). **Test baseline: 2521 passed / 0 failed / 1 ignored** (was 2519; +1 lib test, +1 e2e_pty test → e2e_pty now 7 on Linux). `clippy --all-targets -D warnings` + `fmt --check` clean. The new PTY test was confirmed to **time out without the fix** (genuine guard), green with it. **Issue #39 is closed** (resolution comment + diagnosis recorded on the issue). ## §2. What shipped — issue #39 **Root cause.** `App::update` is pure-sync and validates submissions against `App::schema_cache`. The runtime refreshes that cache **asynchronously** after the worker applies a command — a `SchemaCacheRefreshed` event posted on the **same FIFO channel as key events** (`runtime.rs:1591`, applied `app.rs` `SchemaCacheRefreshed` arm). Under faster-than-human input (paste / script / unpaced PTY) the next Enter is processed *before* the refresh lands, so a simple-mode **Form-B insert** (`insert into T values (…)`, columns derived from the cache) submitted right after `add column` sees the pre-DDL columns, its arity can't match, and the friendly layer tags it *"trying to write SQL?"*. The worker itself is never wrong (it runs commands serially); the bug was purely in client-side pre-validation racing the refresh. **Fix — submission gate (`src/app.rs`).** Two new private `App` fields: `awaiting_schema_refresh: bool` + `held_submissions: VecDeque<(String, EffectiveMode)>`. `dispatch_dsl` arms the flag on **every** `ExecuteDsl` dispatch; while armed, a new DSL submission is **held** (queued in submission order) rather than validated. The `SchemaCacheRefreshed` handler clears the flag and **drains** the queue against the now-fresh cache, stopping as soon as a drained command re-arms the gate (the rest then wait for *its* refresh — order preserved). App-lifecycle commands route through `dispatch_app_command` *before* `dispatch_dsl`, so `quit`/`help`/`load`/`undo`/`rebuild` are never held. **Why arm on every dispatch, not just DDL** — it keeps at most one DSL command in flight, so refreshes are strictly one-per-dispatch and in order, making the boolean provably correct. Arming only on DDL would let a *preceding* non-DDL command's refresh clear the gate early and drain a held insert against a pre-DDL cache (a real corner case). Cost was one existing test (`walking_skeleton::colon_escape_in_simple_mode_is_one_shot`) updated to model the post-dispatch refresh — faithful, since it had been assuming it. **Scope = interactive only.** The `replay` / history-log / startup rebuild-from-text batch path already re-snapshots the schema **synchronously, inline, before every line** (`run_replay` → `build_schema_cache`, `runtime.rs:2514`), so it always had this ordering guarantee; the fix brings the interactive path in line with it. No interactive-user impact (the gate clears in ms); held input is never lost because the runtime sends a `SchemaCacheRefreshed` after **every** dispatch, success or failure (the unconditional post-match block in `spawn_dsl_dispatch`). **Tests** (both verified red→green): - `app::tests::form_b_insert_after_ddl_is_held_until_refresh_then_dispatched` — Tier-1, deterministic: stale cache → submit DDL (arms) → submit insert (held, no error) → deliver fresh `SchemaCacheRefreshed` → insert dispatches. - `e2e_pty::back_to_back_insert_after_ddl_still_succeeds` — Tier-4 PTY, the unpaced inverse of flow 3; asserts the insert's own `('Alice') ✓` echo. **Docs.** ADR-0022 **Amendment 8** records the gate (the §9 schema-cache refresh-vs-validation timing contract); README index updated in the same edit (ADR-0000 rule). ## §3. Open / follow-ups Per the user's direction the open issues come before feature work. After closing #39, **four** issues are open. The hint/help trio (#36/#37/#38) are all **enhancements** on the pedagogy mission (read handoff-76 §7's table for fuller scope; read **ADR-0053** first — it orients #37/#38). **#40** is a CI/packaging follow-up filed this session. | # | One-line | read | | --- | --- | --- | | **#36** | `help ` shows no distinct content — the 7 advanced-mode SQL nodes share `help_id: None` (`src/dsl/grammar/mod.rs:915-918`) | **Contained.** Good next pick; touches H3/`help`, maybe an ADR amendment. | | **#37** | Clause-concept hints (cursor inside `on delete …`, `with pk`, `1:n`/`m:n`, create-table constraint slots) — deferred ADR-0053 extension | **Medium scope, richest teaching value.** Likely an ADR-0053 amendment. | | **#38** | Pre-submit-diagnostic F1 route + ~33 `diagnostic.*` tier-3 blocks; needs a `class`/`message_key` threaded through every diagnostic site | **Broad mechanism, most marginal value.** Get the user's do/defer/close call before investing. | | **#40** | Wire `CHANGELOG.md` into the winget release notes (`komac --release-notes-url`); ADR-0056 area | Filed this session (the changelog→winget thread, see §4). Small, deferred-by-decision originally. | **Suggested order (confirm with the user):** #36 (contained) → #37 (highest on-mission value) → #38 (decision first: do / defer / close). #40 is independent (release pipeline) and can slot in whenever. #38 in particular should be escalated for a do-or-close call rather than silently built. ## §4. Changelog discipline — new rule + open winget thread The session surfaced that **no rule existed** for keeping `CHANGELOG.md` current (it was created in handoff-76's plan but never given a maintenance process), so the #39 fix wasn't logged until the user asked. Decided with the user: - **New `CLAUDE.md` rule (this commit):** update `[Unreleased]` in the **same change** that alters user-facing behaviour (incl. scripted/pasted/power-user paths, not just the interactive happy path), under the two copy rules; no entry for refactor/test/CI-only changes; at release time rename `[Unreleased]` and **sweep commits/handoffs since the last tag** as a backstop. - **#39 entry added** under `[Unreleased] → Fixed`. - **winget release notes → issue #40.** komac supports `--release-notes-url` / `--release-notes`; the `winget` job (`publish.yaml` ≈ L276) passes neither. Tracked, not done (it's a release-pipeline / ADR-0056 change, kept out of this bug-fix session by the user's call). ## §5. Process pins - Commit user-confirmed, no AI attribution, append-only, on `main`; **push is the user's step** (this commit is unpushed). - Test-first honored: both regression tests were confirmed RED before the fix (the PTY one via a `git stash` of `src/app.rs`), GREEN after. A written Devil's-Advocate pass on the implementation surfaced one comment imprecision (fixed) and no behavioral findings. - `cargo sweep` not run this session; the build grew modestly (one extra PTY test binary). Consider a sweep at the next milestone.