> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chartcastr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Save & commit

> Why briefings use an explicit save model, how AI regeneration works on commit, and why row edits are blocked while a commit is in flight.

Briefings use an **explicit save** model. Editing rows, adding items, or renaming things only mutates the draft state on the server — they do **not** call the AI. The AI work happens in one batch when you press **Save**.

## Why explicit save?

The alternative — debounced auto-regen — was rejected because:

* Each save runs N row-analysis calls + 1 overall summary call. Auto-regen on every keystroke would burn tokens on edits the user is still composing.
* Briefings are monthly. There's no "live dashboard" UX expectation; a deliberate commit step matches the cadence.
* It makes the cost story predictable: one bounded burst per save, no surprises.

## What happens on save

When you press Save, the client calls `commitAndRegenerateBriefing(id)`. The server:

1. Creates a new `BriefingContent` row with `status = IN_PROGRESS`, `triggerType = MANUAL`.
2. Snapshots the briefing's row + item composition into `BriefingContent.briefingSnapshot` (so the run is reproducible even if rows change later).
3. **Title pre-step:** if the briefing or any row has unlocked titles AND the title-input hash has changed, makes one batched Haiku call for all titles at once.
4. **Per-row commentary** in parallel (bounded to 3 in flight):
   * Recompute the row's `aiInputsHash`.
   * If it matches the row's existing hash, reuse the prior commentary (no LLM call).
   * Otherwise, fetch the row's items' latest Pulse output and call `generateBriefingRowAnalysis`.
5. **Overall review:** after all rows complete, run `generateBriefingOverallSummary` over the row outputs + item short summaries.
6. Set `BriefingContent.status = SUCCESS`, write `aggregateTokenUsage`, end the run.

The mutation returns immediately with the new `BriefingContent.id`. The composer polls `getBriefing` every 2 seconds until status leaves `IN_PROGRESS`.

<Note>No PDF is rendered and no destination is contacted on a save. PDF and delivery only run on a [snapshot](/analysis/briefings/snapshots).</Note>

## Why row edits are blocked while a commit is in flight

While a `BriefingContent` is `IN_PROGRESS` for a briefing, every `mutateBriefingRow` call is rejected with `BRIEFING_COMMIT_IN_PROGRESS`. The composer disables the row-edit affordances (drag handles, item picker, title fields) for the same reason.

The lock exists because:

* The snapshot was frozen at commit start. Downstream row analyses run against that frozen view.
* Allowing edits during the run would silently invalidate the row hashes and produce stale-on-arrival results.
* A typical commit takes \<30 seconds. A short read-only window is a clearer UX than letting writes race.

The lock is **per-briefing only** — other briefings remain freely editable, and any unrelated mutation (sources, connections, settings) is unaffected.

### Stale runs

If a commit somehow gets stuck (server crash, network partition), a 30-minute sweeper transitions abandoned `IN_PROGRESS` runs to `FAILED` so the briefing isn't permanently locked.

## The `needsCommit` flag

`Briefing.needsCommit` is the single source of truth for "this briefing has uncommitted changes". The server computes it from row-level `aiInputsHash` mismatches — **don't** derive it on the client from `updatedAt` timestamps. The composer's "Unsaved changes" badge reads this field directly.

`Send Snapshot` is disabled when:

* `needsCommit === true` — there's nothing fresh to send,
* `isCommitting === true` — a save is in flight, or
* The monthly throttle says next-eligible-date is in the future.

## What changes trigger AI work?

| Edit                       | Title regen?              | Row commentary regen? | Overall review regen? |
| -------------------------- | ------------------------- | --------------------- | --------------------- |
| Add a row                  | ✅ (new row needs a title) | ✅ (new row only)      | ✅ (always)            |
| Add an item to a row       | ✅ (row title may shift)   | ✅ (just that row)     | ✅                     |
| Remove an item             | ✅                         | ✅ (just that row)     | ✅                     |
| Reorder items within a row | ❌                         | ❌                     | ✅                     |
| Reorder rows               | ❌                         | ❌                     | ✅                     |
| Type a row title           | ❌                         | ❌                     | ✅                     |
| Edit briefing description  | ✅                         | ❌                     | ✅                     |
| Reset to AI title          | ✅                         | ❌                     | ✅                     |

The overall review **always** runs because it's a single small call and depends on every row's output.
