---
description: Internal — Local-mode CRUD for operational state (sessions, tasks, phase bypasses). Direct SQLite, no Librarian.
---

# local/state-crud (internal)

Local-mode replacement for the Memex `state-crud` recipe. Operational
state — work-in-progress sessions, task status, phase-bypass audit
trail — is plain SQL on the project-local `<workspace>/.ai/atelier.db`.
No Librarian, no document indirection, no FTS5 (state rows are not
searchable text; if you need a row to be searchable, also call
`backend_local.write_document`).

## When to read this

You are running in Local mode and need to mutate one of:

- `sessions` — per-(project, agent) work-in-progress record
- `projects.phase` — phase progression
- `tasks.status` — pending → in-progress → done
- `phase_bypasses` — append-only audit of soft-wall overrides

## Recipes

### Upsert a session

```python
backend_local.upsert_session(
    *,
    project_id: int,
    agent_id: str,
    phase: str | None = None,
    current_tasks: str | None = None,
    accomplished: str | None = None,
    next_action: str | None = None,
    status: str = "in-progress",
    pm_notes: str | None = None,
) -> dict
```

Finds the `(project_id, agent_id, status='in-progress')` row. If it
exists, UPDATE only the non-None fields (plus `updated_at`). Otherwise
INSERT. Returns the full row as a dict.

### Transition a project's phase

```python
backend_local.transition_phase(
    *, project_id: int, to_phase: str,
    agent_id: str, bypass_reason: str | None = None,
) -> dict
```

Updates `projects.phase` and `projects.updated_at`. Returns the updated
row. Note: this does **not** validate the transition graph — that is
`scripts.workflow.advance_phase`'s job, and the facade-level dispatcher
is expected to call workflow validation first. If you bypassed the gate,
also call `record_phase_bypass` to log it.

### Update a task's status

```python
backend_local.update_task_status(
    *, task_id: int, status: str, notes: str | None = None,
) -> dict
```

Sets `tasks.status` (and optionally `tasks.notes`), bumps `updated_at`,
returns the updated row. The valid status values come from the workflow
domain vocabulary (`pending`, `assigned`, `in-progress`, `done`,
`cancelled`); this function does not enforce them — pass through what
the caller hands you and let the workflow layer reject invalid
transitions.

### Record a phase bypass

```python
backend_local.record_phase_bypass(
    *, project_id: int, from_phase: str, to_phase: str,
    reason: str, agent_id: str,
) -> dict
```

Append-only INSERT into `phase_bypasses`. Returns the new row including
the autogenerated `id` and `created_at`. The `dev-handoff` retro queries
this table — never DELETE from it.

## Hard rules

- All four entry points open and close a fresh connection via `_conn()`
  (WAL + FK enforcement). Do not hold connections across calls.
- `phase_bypasses` is append-only. No UPDATE, no DELETE.
- `upsert_session` finds the **in-progress** row, not the most recent.
  Closed sessions (`status != 'in-progress'`) are immutable history.
- These functions do **not** write to `documents` or `documents_fts`.
  Operational state is not searchable wiki content. If you want a task or
  phase event to appear in `find_documents`, that is what `write_task`
  and `write_meeting` already do — they call `write_document` internally
  for you.
