Sessions live in the store: started, listed, read, patched and messaged through the API, their runs claimed at once outside a runner's capacity #204

Merged
agra merged 7 commits from task/3905-s1-sessions-in-the-store-a-session-its-l into master 2026-10-31 15:54:40 +02:00
Owner

Tracker: #3905

Step S1 of the Sessions plan (#3893).

What it delivers

  • The migration (58). A session (in one project or in Global, its title, model, effort, permission mode, runner, branch, pin, archive and revision), its session_legs (one harness conversation each, with the harness's conversation id) and its session_turns (one message of Agra's each, the run that takes it and how it ended). run_request and stream are rebuilt so each belongs to a task or to a session, and a session's run serves one of its legs; a stream's harness conversation moves to conversation_id, still answered as the stream's session_id.
  • The session routes (docs/api.md, new section Sessions): POST /api/sessions answers 201 with the session, its first turn and the run it queued; GET /api/sessions pages them, the latest active first, under ?project=, ?global, ?q=, ?pinned and ?archived; GET /api/sessions/:id reads one with its legs, runs and turns; PATCH /api/sessions/:id sets title, pinned, archived and permission under If-Match; POST /api/sessions/:id/messages makes the next turn, keyed like an inbox message; POST /api/sessions/:id/streams opens a session's run's stream.
  • A session is titled from its first message: its whole words that fit in 60 characters. A session in a project works on session/<id>-<slug> or the branch it is given; one in Global works in no checkout.
  • A session's runs never queue behind anything. Each plays role session, takes no place in the claim order, and is offered at once, with session_id and no task, priority or place, to the runner the session names or to each connected runner that meets its project. The runner claims it as soon as it is offered, with every place of its capacity taken, and it takes none; holding does not count it. Its claim starts the session's next leg on the harness it resolved.
  • Turns ride the runs. A message goes into the inbox of the session's run going while its stream runs, waits for that run's start otherwise (its start pushes the stream's waiting messages), and queues a run of its own when none goes. When a session's run ends, the turns that reached its harness end with it, and the rest move to a new run, offered at once.
  • A session's stream reads task_id null; its run names the session. The runner opens it at POST /api/sessions/:id/streams; a session's run is given no SAK_REPORT, and one in Global no gate cache. GET /api/runs and GET /api/streams keep to the tasks' runs and streams, so nothing the web reads changes. A stream that names a run is opened on that run's own task or session: a task's stream naming a session's run, or another task's, answers 400.
  • A memory a session's run saves is its project's, or global and held for one in Global; its instructions say a memory is for a rule beyond the session.

Checks

  • tests/sessions.sx: the routes, their refusals, the offers on the runner channel (to the meeting runners, the named runner alone, every runner for Global), the claim beside a task's run with holding unmoved, the stream on the session, turns into the inbox at once or at the run's start, the turns' ends and the move of an untaken turn to a new run, the list's filters and order, the patch under If-Match, and a Global session's memory.
  • tests/runner_priority.sx: a session's ticket starts with every place of the runner taken while a run of the claim order waits, and one going takes none of the capacity.
  • tests/runner_core.sx: on a runner of capacity one running a step, a session's run is claimed and run to its end beside it on the stub harness; its stream reads no task and carries the conversation its leg takes.
  • tests/deployed_upgrade.sx: the deployed release's database migrates, each stream's harness conversation moving to conversation_id. The schema tests that take a fresh database back to an older version first take it back to 57 (tests/support/schema.sx).

The review's regression: tests/sessions.sx checks that POST /api/tasks/:id/streams naming a session's run answers 400 ("a task's stream names no run of a session"). Before the fix, on a689be0d's store, the check fails: the open answers 201 with a stream on the task recording the session's run, the session's own open then answers 200 with that stream, and the run's start answers 400, so SESSIONS FAIL (10 checks). After it, at b20b2270, SESSIONS OK.

No bug QA reported is fixed here. It changes no screen, so it adds and changes no golden under web/e2e.

Gates at the head

  • sak-gate full, gate run 1, at b20b2270: passed (73 sx tests, the release check and tests/step9_release.sx, the deploy contract, and both Playwright configs, 799 passed with 1 skipped, and 10 passed).
Tracker: [#3905](https://sak.swipelab.com/tasks/3905) Step S1 of the Sessions plan ([#3893](https://sak.swipelab.com/tasks/3893)). ## What it delivers - **The migration (58).** A `session` (in one project or in Global, its title, model, effort, permission mode, runner, branch, pin, archive and revision), its `session_leg`s (one harness conversation each, with the harness's conversation id) and its `session_turn`s (one message of Agra's each, the run that takes it and how it ended). `run_request` and `stream` are rebuilt so each belongs to a task or to a session, and a session's run serves one of its legs; a stream's harness conversation moves to `conversation_id`, still answered as the stream's `session_id`. - **The session routes** (docs/api.md, new section Sessions): `POST /api/sessions` answers `201` with the session, its first turn and the run it queued; `GET /api/sessions` pages them, the latest active first, under `?project=`, `?global`, `?q=`, `?pinned` and `?archived`; `GET /api/sessions/:id` reads one with its legs, runs and turns; `PATCH /api/sessions/:id` sets `title`, `pinned`, `archived` and `permission` under `If-Match`; `POST /api/sessions/:id/messages` makes the next turn, keyed like an inbox message; `POST /api/sessions/:id/streams` opens a session's run's stream. - **A session is titled from its first message**: its whole words that fit in 60 characters. A session in a project works on `session/<id>-<slug>` or the branch it is given; one in Global works in no checkout. - **A session's runs never queue behind anything.** Each plays role `session`, takes no place in the claim order, and is offered at once, with `session_id` and no task, priority or place, to the runner the session names or to each connected runner that meets its project. The runner claims it as soon as it is offered, with every place of its capacity taken, and it takes none; `holding` does not count it. Its claim starts the session's next leg on the harness it resolved. - **Turns ride the runs.** A message goes into the inbox of the session's run going while its stream runs, waits for that run's start otherwise (its start pushes the stream's waiting messages), and queues a run of its own when none goes. When a session's run ends, the turns that reached its harness end with it, and the rest move to a new run, offered at once. - **A session's stream reads `task_id` `null`; its run names the session.** The runner opens it at `POST /api/sessions/:id/streams`; a session's run is given no `SAK_REPORT`, and one in Global no gate cache. `GET /api/runs` and `GET /api/streams` keep to the tasks' runs and streams, so nothing the web reads changes. A stream that names a run is opened on that run's own task or session: a task's stream naming a session's run, or another task's, answers `400`. - A memory a session's run saves is its project's, or global and held for one in Global; its instructions say a memory is for a rule beyond the session. ## Checks - `tests/sessions.sx`: the routes, their refusals, the offers on the runner channel (to the meeting runners, the named runner alone, every runner for Global), the claim beside a task's run with `holding` unmoved, the stream on the session, turns into the inbox at once or at the run's start, the turns' ends and the move of an untaken turn to a new run, the list's filters and order, the patch under `If-Match`, and a Global session's memory. - `tests/runner_priority.sx`: a session's ticket starts with every place of the runner taken while a run of the claim order waits, and one going takes none of the capacity. - `tests/runner_core.sx`: on a runner of capacity one running a step, a session's run is claimed and run to its end beside it on the stub harness; its stream reads no task and carries the conversation its leg takes. - `tests/deployed_upgrade.sx`: the deployed release's database migrates, each stream's harness conversation moving to `conversation_id`. The schema tests that take a fresh database back to an older version first take it back to 57 (`tests/support/schema.sx`). The review's regression: `tests/sessions.sx` checks that `POST /api/tasks/:id/streams` naming a session's run answers `400` ("a task's stream names no run of a session"). Before the fix, on `a689be0d`'s store, the check fails: the open answers `201` with a stream on the task recording the session's run, the session's own open then answers `200` with that stream, and the run's start answers `400`, so `SESSIONS FAIL (10 checks)`. After it, at `b20b2270`, `SESSIONS OK`. No bug QA reported is fixed here. It changes no screen, so it adds and changes no golden under `web/e2e`. ## Gates at the head - `sak-gate full`, gate run 1, at `b20b2270`: passed (73 sx tests, the release check and `tests/step9_release.sx`, the deploy contract, and both Playwright configs, 799 passed with 1 skipped, and 10 passed).
agra merged commit 0b89e53a67 into master 2026-10-31 15:54:40 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sx/sak!204
No description provided.