An ungrouped session or a whole group can be pinned. Pinned things sit above a rule at the top of the sidebar, and the idle window stops ageing them out. A session inside a group cannot be pinned — its group is the thing you pin. Screens are the running branch, not a mock.

release pinned whole, and the ungrouped session infra. The group fleet and the ungrouped deploy are unpinned, below the rule. Members stay with their group wherever it sits.


× that was already there. Pin comes first: the reversible everyday control before the one that files the agent away.


There are two roads into the archived view and they mean different things. Pinning blocks one of them and leaves the other completely alone.
| Road | What it is | Pinning |
|---|---|---|
The idle window (archive.after) | Derived. "Nothing has happened here for a month." Changes the moment the setting changes. | Suppressed. This is what "pinned sessions don't get archived after some time" means. |
Archiving (archivedAt) | Stored. "I am finished with this one." Stops the agent, and is the only road that unlocks delete. | Untouched. A pinned session can still be archived deliberately — and doing so clears the pin. |
Archiving clears the pin because the alternative is a record that asserts "keep this in front of me" and "I am done with this" at the same time. The later statement is the true one. Restoring it later brings it back unpinned rather than silently re-pinning something the operator last saw as finished.
An ungrouped session, or a whole group. A session inside a group cannot be pinned at all — decided by the operator after seeing the alternative built.
| Option | Cost | |
|---|---|---|
| A grouped session cannot be pinned. Its group is what you pin. | The pin is unavailable on a row that has one; you pin the group instead, which moves its siblings too | chosen |
| It rises out of its group to the pinned block, tagged with the group it came from | A group that visibly shows one fewer row than it has members, and a row that appears to have left the group it is still in. This was built, reviewed, and rejected as odd to use. | rejected |
| Shown in both places | Two live rows on screen for one session, each with its own state and controls | rejected |
| It stays in place, pinned but unmoved | Pinning that does nothing visible | rejected |
The lifting mechanism this replaced is gone rather than disabled — no code, no affordance, no tests for a behaviour that no longer exists. What remains is one predicate: a ref can be pinned if it is a group, or a session in no group.
The control is absent, not disabled. A disabled pin on every member would repeat what the group header's single pin already says, once per member, and argue back when clicked. On touch — where every row's controls are visible at once — the layout teaches the rule by itself: pins on group headers and ungrouped rows, never on a member.
It holds the group's place and exempts every member from the idle window. Position alone is not enough: a pinned group whose agents each age out on their own schedule empties one row at a time and then disappears entirely — the sidebar drops a group once all its agents are archived. Pinning it would have caused exactly what it was asked to prevent.
Members do not get a pinnedAt of their own; membership is what carries them. So unpinning the group hands every member straight back to the ordinary rules, and a member who was also pinned in its own right stays pinned.
Pinning does not add a second ordering. It partitions the one manual order and keeps each half in it, so there is nothing for the two to disagree about. Drag a row within a block and it reorders exactly as before.
Dragging across the rule is the case that would otherwise let them fight, and the answer is that a dropped row takes the pin state of whatever it lands beside — the same way a row dropped into a group takes that group's membership:
The rule is a boundary the operator can drag across, rather than a line that silently snaps their drop back where it came from. Dropping onto a group is not a neighbour relationship and is left alone: membership decides which block draws it from then on.
It does here, and the codebase had already answered it. Below 720px, .row-actions stops being hover-only and becomes permanently visible — the stylesheet already does this, with a comment saying why it had to for the group controls: "hover-only … a touch device can never satisfy: on a phone they were simply unreachable." The .age column gives up its space to make room. The pin joins that strip and inherits all of it.
| Measured | Desktop (1120px) | Touch (390px) |
|---|---|---|
| controls at rest | opacity 0 on every row | opacity 1 on every row |
| controls on hover | opacity 1 on the hovered row only | n/a |
| row overflow at 390px | — | none, on any row |
One honest caveat: the pin's tap target is 25px, the same as the archive × beside it. That is below the usual 44px guidance — but it is the size these controls already are, and this change does not make it worse. Enlarging them is a separate decision about the whole strip.
Dragging into a group clears the pin. Same rule as the boundary above: a dropped row takes the pin state of where it lands, and it has landed somewhere pins cannot exist. Without this the pin would sit there invisibly and reappear the moment the row was dragged back out.
Dragging out of a group leaves it unpinned. A grouped session reads as unpinned wherever the pin is consulted, whatever its stored record happens to say — so coming out it takes the state of wherever it lands, never a stale true. Dropping it deliberately into the pinned block does pin it, which is the operator asking for that.
A stray pinnedAt on a grouped session is ignored, not rendered. Such a record describes a state the rule says cannot exist — written before the session joined a group, or by a path that did not clear it. It buys no exemption from the idle window, and it cannot put the row above the rule: order holds top-level refs only, so a grouped session is not in it and there is no code that could draw one there. Verified against a real stray record left over from testing: the row stayed with its group and had no pin control.
Stored, next to the archive state, exactly as asked — pinnedAt on the session record and on the group record. Not derived, so it survives a restart and means the same thing on every device. Four routes: POST /api/sessions/:id/pin and /unpin, POST /api/groups/:id/pin and /unpin. One call per direction rather than a toggle, so a double click or a stale row cannot flip it to the opposite of what was on screen.
A group's pin is set through its own function rather than through update(), which is the shape the tree editor sends — otherwise renaming a group would clear a pin it never knew about. There is a test for exactly that.