Skip to content

feat(ui): name snapshots, and put fork, rename and delete on their mark - #2847

Merged
EItanya merged 8 commits into
mainfrom
charlesthebird/snapshotDetails
Sep 17, 2026
Merged

EItanya merged 8 commits into
mainfrom
charlesthebird/snapshotDetails

Conversation

@Charlesthebird

@Charlesthebird Charlesthebird commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

🤖 written by Claude (start)

"Checkpoint" is "Snapshot" in the chat. A snapshot carries a name, its mark on the transcript carries Fork, Rename and Delete, and pressing the mark opens the record behind it.

Change Detail
A snapshot has a name Checkpoint.name, generated on creation as <agentInstanceId>-<headTaskId>.
Renaming one UpdateCheckpointName, mirroring UpdateAgentInstanceName. An empty name restores the generated default, so a fork always has something to be called.
A fork takes the snapshot's name The chat no longer renames after forking, because ForkAgentInstance copies the checkpoint's name onto the new conversation.
The mark carries its actions Fork, Rename and Delete sit under it; pressing the mark itself opens the record, which holds the id, turn, state and age.
Two extension points app_agents_agentChat_snapshotDivider_actions beside those controls, and app_agents_agentChat_snapshotDetails_footer at the foot of the record. Both take { snapshotId, instanceId, label }.

The name lives in the data payload with the rest of the checkpoint, and the source_name column is gone. Nothing filters or sorts checkpoints by name, so a column earned it nothing and cost a second place for the value to live.

Testing

  1. Chat with an agent for two turns, then take a snapshot from the composer.
  2. The mark reads Snapshot "<instance id>-<turn id>", clipped if it is long. Hovering offers to open the record; the full name is on the record itself.
  3. Send a third turn and take a second snapshot.
  4. Rename the first one from its mark, reload the page, and press the mark. The new name is on the record.
  5. Fork from that mark. The new conversation opens named after the snapshot, holding only the turns above it.
  6. Check both themes: the mark is a tinted card, and Rename's outline stays legible on each.

🤖 written by Claude (end)

…elete

"Checkpoint" is "Snapshot" in the chat, and the marker on the transcript
opens a details modal rather than carrying its own buttons.

A snapshot now has a name, generated at creation from the conversation and
the turn it sits at, and a fork is given that name. The column that already
held the name a fork receives, `agent_instance_checkpoint.source_name`, is
renamed `name` rather than a second one being added beside it.

The schema change is folded into the unreleased baseline migration, so a
development database has to be recreated.

Signed-off-by: Nicholas Bucher <[email protected]>
@github-actions github-actions Bot added the enhancement New feature or request label Sep 16, 2026
@Charlesthebird

Copy link
Copy Markdown
Collaborator Author
summary-reel.mp4

🤖 written by Claude

The mark names itself — Snapshot "<name>" — and pressing it opens the
record behind it. Fork, Rename and Delete sit under it, and an extension
point sits beside them.

The mark is a tinted card rather than a rule, at a strength per theme: the
accent pair is several steps above a near-black page on dark and within a
whisker of white on light, so one value cannot be a mark on both.

Signed-off-by: Nicholas Bucher <[email protected]>
@Charlesthebird

Charlesthebird commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

Claude was trying to update the source_name column on the checkpoint to be called name. I told it to reuse source_name so that we don't have migrations or schema changes in this PR.

Reuses the existing `source_name` column rather than renaming it, so the
unreleased baseline migration is untouched and no development database has
to be recreated. The column's job is the same one it always had — the name
a fork is given — and only what fills it has changed.

Also from review: the name no longer reaches the `data` payload, where a
rename left it permanently disagreeing with the column; renaming honours the
READY filter that Get and List both apply; the two snapshot extension points
agree on `label`; and a mark whose record has not arrived yet no longer
offers a button that opens nothing.

Signed-off-by: Nicholas Bucher <[email protected]>
@Charlesthebird Charlesthebird changed the title feat(ui): name snapshots, and open one's details to fork, rename or delete feat(ui): name snapshots, and put fork, rename and delete on their mark Sep 16, 2026
@Charlesthebird

Copy link
Copy Markdown
Collaborator Author
summary-reel.mp4

🤖 written by Claude

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Sep 16, 2026
Delete took its red from antd's generated danger palette, which is rebuilt
from the config when the theme changes and lands a frame after the two
Emotion-styled buttons beside it — so mid-switch it was still wearing the
theme just left. It now reads the danger token directly, as they do.

Signed-off-by: Nicholas Bucher <[email protected]>
@Charlesthebird

Copy link
Copy Markdown
Collaborator Author
summary-reel.mp4

🤖 written by Claude

@Charlesthebird
Charlesthebird marked this pull request as ready for review September 16, 2026 19:10
@Charlesthebird
Charlesthebird requested review from a team and peterj as code owners September 16, 2026 19:10
The pointer that opens the confirmation is still resting on Delete, so its
tooltip stayed up and the tooltip's container sat over the confirmation's own
buttons — a reader could not press Cancel. CI saw it as a flaky click.

What the tooltip said, the confirmation already says.

Signed-off-by: Nicholas Bucher <[email protected]>

@EItanya EItanya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated review.

Reviewed the Go changes at 90dadcde8969951d167ea25f90a0c2cc9bba0ff8 for correctness, code cleanliness, and structure. The RPC/service/store boundaries look sensible; the main simplification is in checkpoint persistence.

  1. Use Checkpoint.Name as the single source of truth and remove source_name.

    The new serializer clones the protobuf, removes Name, and reconstructs it from SourceName on reads. SourceName now means the checkpoint's own name, requiring a lengthy comment explaining the misleading field name.

    Breaking schema changes are allowed right now, so preserving this column does not justify introducing a second persistence convention. The “so rename is one UPDATE” explanation also does not require this arrangement: the existing instance rename already updates the protobuf with one UPDATE under a transaction. No query filters or sorts checkpoints by name.

    Please persist Checkpoint.Name in the protobuf, follow the existing transactional rename pattern, and have forks inherit the decoded checkpoint's name. Remove source_name/SourceName, marshalCheckpoint, the read-side name override, and the comments explaining those exceptions. That keeps one representation without compatibility branches or duplicate storage.

  2. Preserve source-payload validation when removing the source-name lookup.

    Removing the toAgentInstance(instance) call also removes validation of the stored source protobuf.

    I reproduced this against PostgreSQL by creating an instance and a valid completed task/snapshot, then deliberately replacing only the instance payload with the invalid byte 0xff. GetAgentInstance rejects the payload, but checkpoint reservation succeeds and commits a CREATING checkpoint. Restoring just the decoding guard makes the probe pass.

    This concerns already-corrupt durable data; ordinary renaming does not cause the corruption. Please preserve that validation independently of reading the source name.

  3. Make the default-name tests independent of the implementation helper.

    The creation and reset assertions compute their expectations with the same defaultCheckpointName helper used by production. Returning an incorrect constant from that helper would leave those assertions green. Use explicit expected strings to verify the naming contract. Also cover rename rejecting CREATING, FAILED, and DELETING checkpoints.

The transport handler, authorization policy, service delegation, and MCP field addition look appropriately scoped; no additional abstractions are needed there.

Validation: all five relevant Go packages passed, including PostgreSQL persistence and inline SQL preparation checks:

go test ./core/internal/service/checkpoint ./core/internal/grpcserver ./core/internal/mcp
go test ./core/internal/database ./core/pkg/migrations

The additional corruption probe fails on the reviewed head and passes with only the removed validation restored. I did not run live-cluster E2E tests, UI tests, or local lint/generation checks.

The name was split out into the `source_name` column and stripped from the
protobuf, so every read had to put it back and the column's name no longer
described what it held. One representation instead: `Checkpoint.Name` is
persisted like every other field, rename follows the transactional pattern
`UpdateAgentInstanceName` already uses, and forks take their name from the
decoded checkpoint.

Reservation decodes the source instance again. That call went when the source
name lookup did, and it was also the guard rejecting a corrupt stored payload —
without it a checkpoint could be reserved against an instance that cannot be
read.

The default-name assertions now compare against literal strings, so a wrong
constant in `defaultCheckpointName` fails them, and rename is covered against
creating, failed and deleting checkpoints.

Signed-off-by: Nicholas Bucher <[email protected]>
@blacksmith-sh

This comment has been minimized.

Both claimed the tooltip returns the half of a long name the row clips. It
reads "Open snapshot details." and always has. The full name is on the record
and in the row's aria-label.

Signed-off-by: Nicholas Bucher <[email protected]>
@Charlesthebird

Copy link
Copy Markdown
Collaborator Author
snapshot-demo.mp4

🤖 written by Claude

@Charlesthebird

Copy link
Copy Markdown
Collaborator Author

All three done in 4f7afeb5.

  1. Checkpoint.Name now persists in the payload. The source_name column is gone. Rename follows UpdateAgentInstanceName under one transaction. Forks inherit the decoded checkpoint's name. marshalCheckpoint and the read-side override are deleted.
  2. toAgentInstance(instance) is back in reservation. A test writes 0xff and expects rejection.
  3. Expectations are literal strings now. Rename rejection covers CREATING, FAILED and DELETING.

🤖 written by Claude

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Sep 17, 2026
The comment asked callers to pass a parsed id rather than the string that
arrived on the wire, so a differently-cased id would generate a different
default name. The parameter type says it instead, and both callers already
held a uuid.UUID they were converting down.

Signed-off-by: Nicholas Bucher <[email protected]>
@Charlesthebird

Copy link
Copy Markdown
Collaborator Author

Re-recorded against f75b14c8. Step 2 now matches the build: hovering the mark offers to open the record, and the full name is on the record rather than in a tooltip. Supersedes the earlier reel.

snapshot-demo-2847.mp4

🤖 written by Claude

@EItanya
EItanya added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit ff355b3 Sep 17, 2026
27 of 28 checks passed
@EItanya
EItanya deleted the charlesthebird/snapshotDetails branch September 17, 2026 19:36
Rachael-Graham added a commit to kagent-dev/website that referenced this pull request Sep 23, 2026
kagent-dev/kagent#2847 renamed the chat's checkpoint to "Snapshot", gave
it a generated name, and moved Fork, Rename and Delete onto the snapshot's
mark. The refreshed capture in this PR shows that card; the prose still
described the old Checkpoint/Fork/Remove strip, and the alt text promised
a tool call that the taller card pushed out of frame.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Signed-off-by: Rachael Graham <[email protected]>
teemow added a commit to giantswarm/kagent-upstream that referenced this pull request Sep 25, 2026
Kagent 1.0 recorded core version 1 with an earlier 000001 baseline, and
000001 has since changed in place (upstream kagent-dev#2847, kagent-dev#2857, kagent-dev#2868). Goose
sees version 1 applied on a database created by 1.0 and runs nothing, so
the controller finds no runtime_revision.credentials, no agent instance
operation_id and executor_id, no generated pinned_checkpoint_id and no
DELETED state, and fails to store a runtime revision with
column "credentials" of relation "runtime_revision" does not exist.

000002 tells the two baselines apart by the four columns the current one
added: none of them present is the 1.0 baseline, which it upgrades with
the current baseline's columns, constraints (under the names a fresh
install gives them) and partial index; all of them present is a database
the current 000001 created, which it leaves unchanged; anything else is
neither baseline and fails the migration, in its transaction.

agent_instance_checkpoint.source_name stays: the 1.0 controller reads and
writes it in every checkpoint query and the current controller never
names it, so a migrated database still runs 1.0 and nothing is lost. The
Down section has nothing to undo for the same reason.

TestCoreUpgradesThe1_0Baseline replays the 1.0 baseline (a copy of it in
testdata/core-1.0) with rows, runs the builtin migrations over it and
compares the schema with a fresh install's, which 000002 must leave as
000001 made it; TestCoreRefusesAMixedBaseline proves the refusal.
TestBuiltinMigrationsRoundTrip expects every migration a source ships.
teemow added a commit to giantswarm/kagent-upstream that referenced this pull request Sep 25, 2026
Kagent 1.0 recorded core version 1 with an earlier 000001 baseline, and
000001 has since changed in place (upstream kagent-dev#2847, kagent-dev#2857, kagent-dev#2868). Goose
sees version 1 applied on a database created by 1.0 and runs nothing, so
the controller finds no runtime_revision.credentials, no agent instance
operation_id and executor_id, no generated pinned_checkpoint_id and no
DELETED state, and fails to store a runtime revision with
column "credentials" of relation "runtime_revision" does not exist.

000002 tells the two baselines apart by the four columns the current one
added: none of them present is the 1.0 baseline, which it upgrades with
the current baseline's columns, constraints (under the names a fresh
install gives them) and partial index; all of them present is a database
the current 000001 created, which it leaves unchanged; anything else is
neither baseline and fails the migration, in its transaction.

agent_instance_checkpoint.source_name stays: the 1.0 controller reads and
writes it in every checkpoint query and the current controller never
names it, so a migrated database still runs 1.0 and nothing is lost. The
Down section has nothing to undo for the same reason.

TestCoreUpgradesThe1_0Baseline replays the 1.0 baseline (a copy of it in
testdata/core-1.0) with rows, runs the builtin migrations over it and
compares the schema with a fresh install's, which 000002 must leave as
000001 made it; TestCoreRefusesAMixedBaseline proves the refusal.
TestBuiltinMigrationsRoundTrip expects every migration a source ships.
alexdarbyshire pushed a commit to alexdarbyshire/kagent that referenced this pull request Oct 5, 2026
…rk (kagent-dev#2847)

Signed-off-by: alexdarbyshire <[email protected]>

---
*🤖 written by Claude (start)*

"Checkpoint" is "Snapshot" in the chat. A snapshot carries a name, its
mark on the transcript carries Fork, Rename and Delete, and pressing the
mark opens the record behind it.

| Change | Detail |
|---|---|
| A snapshot has a name | `Checkpoint.name`, generated on creation as
`<agentInstanceId>-<headTaskId>`. |
| Renaming one | `UpdateCheckpointName`, mirroring
`UpdateAgentInstanceName`. An empty name restores the generated default,
so a fork always has something to be called. |
| A fork takes the snapshot's name | The chat no longer renames after
forking, because `ForkAgentInstance` copies the checkpoint's name onto
the new conversation. |
| The mark carries its actions | Fork, Rename and Delete sit under it;
pressing the mark itself opens the record, which holds the id, turn,
state and age. |
| Two extension points | `app_agents_agentChat_snapshotDivider_actions`
beside those controls, and `app_agents_agentChat_snapshotDetails_footer`
at the foot of the record. Both take `{ snapshotId, instanceId, label
}`. |

The name lives in the `data` payload with the rest of the checkpoint,
and the `source_name` column is gone. Nothing filters or sorts
checkpoints by name, so a column earned it nothing and cost a second
place for the value to live.

## Testing

1. Chat with an agent for two turns, then take a snapshot from the
composer.
2. The mark reads `Snapshot "<instance id>-<turn id>"`, clipped if it is
long. Hovering offers to open the record; the full name is on the record
itself.
3. Send a third turn and take a second snapshot.
4. Rename the first one from its mark, reload the page, and press the
mark. The new name is on the record.
5. Fork from that mark. The new conversation opens named after the
snapshot, holding only the turns above it.
6. Check both themes: the mark is a tinted card, and Rename's outline
stays legible on each.

---
*🤖 written by Claude (end)*

---------

Signed-off-by: Nicholas Bucher <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants