Skip to content

feat: edit message - #1653

Open
maekawataiki wants to merge 1 commit into
mainfrom
feat/edit-message
Open

feat: edit message#1653
maekawataiki wants to merge 1 commit into
mainfrom
feat/edit-message

Conversation

@maekawataiki

Copy link
Copy Markdown
Collaborator

Description of Changes

Extends the message edit feature (#1120) so that a message in the middle of a conversation can be edited, and fixes the data consistency issues that blocked it.

Problem

The edit feature only allowed editing the last user message. Extending it to earlier messages surfaced two issues in the existing implementation:

  1. Discarded messages were left in DynamoDB. Truncation was applied to the client
    state only, and there was no API to delete messages. On reload, the discarded
    messages came back and were mixed with the regenerated response (the edited user
    message keeps its original createdDate while the new response gets a newer one,
    so the restored order was broken).
  2. The assistant message was recorded twice. The regenerated assistant message was
    pushed to toBeRecordedMessages by addMessageIdsToUnrecordedMessages() and then
    again by the generationMode === 'edit' branch, producing two DynamoDB items with
    the same messageId and different createdDate.

Changes

Backend

  • Add DELETE /chats/{chatId}/messages?fromCreatedDate=...
  • deleteMessagesFrom() in repository.ts queries the keys with createdDate >= :fromCreatedDate and deletes them with chunked BatchWriteItem (same pattern as the existing deleteChat())
  • The handler returns 400 without fromCreatedDate and 403 when the chat belongs to another user (findChatById())
  • Token usage already recorded in the stats table is intentionally not rolled back, because the tokens were actually consumed
  • messages.addMethod('DELETE') is declared explicitly in api.ts because the addProxy() catch-all does not apply to methods of an already declared Resource. API_DEPLOYMENT_VERSION is bumped to v3 accordingly.

Frontend

  • useChat.edit() now takes the target messageId, truncates the conversation at that message, and deletes the discarded messages from the table before updating the local state. The deletion range starts at the message after the target, so if the regeneration fails the stored conversation is still consistent (truncated at the edited message, with its original content).
  • Validation moved out of the produce() recipe: if the target is not found, an error toast is shown and the regeneration is not started (previously it silently fell through and regenerated on top of the existing response).
  • loading is set while the delete request is in flight so the conversation cannot be modified underneath the operation.
  • The assistant message is no longer recorded twice (the edit case is removed from the assistant update branch).
  • Messages without a messageId (not recorded yet) are not editable, to avoid editing the wrong message through the fallback path.
  • A confirmation dialog is shown only when there are messages after the edited one, so
    editing the last message stays a single click as before.

Scope

Use case Editable messages
Chat any user message
RAG Chat (Knowledge Base) last user message only (unchanged)
Agent Chat last user message only (unchanged)

RAG Chat (KB) and Agent Chat send only the last message plus a sessionId (RetrieveAndGenerateStream / InvokeAgent), so the conversation history lives in the server-side session. Truncating the conversation on the client cannot be reflected there, which means the model would keep answering based on messages the user has just deleted. RetrieveAndGenerate has no equivalent of sessionState.conversationHistory, so a truncated history cannot be seeded either, and resetting the session would drop the whole context. Middle-of-conversation editing is therefore enabled only on the plain Chat page, where the full message list is sent on every request. Editing the last message behaves as before on all three pages.

Impact on existing users

  • Editing a message in the middle of a conversation deletes all the messages after it. This is irreversible, so a confirmation dialog was added. Branching (keeping the previous conversation as an alternative path) is out of scope for this PR.
  • On edit, the regenerated assistant message is now stored as a new item (new messageId) instead of being updated in place. Feedback on that message was already reset by batchCreateMessages() before this change, so the visible behavior is the same.
  • New i18n keys: chat.edit_message_confirmation, chat.edit_message_warning, error.editFailed, error.editTargetNotFound (added for all 6 languages).

Checklist

  • Modified relevant documentation
  • Verified operation in local environment
  • Executed npm run cdk:test and if there are snapshot differences, execute npm run cdk:test:update-snapshot to update snapshots

Tests

  • packages/cdk/test/lambda/deleteMessages.test.ts: 204 / 400 / 403 / 500
  • packages/web/tests/components/ChatMessage.test.tsx: the edit button is shown for any
    recorded user message, and hidden when the message has no messageId or is not editable
  • npm run lint, npm run web:test (281 passed), npx jest test/lambda (34 passed)

Related Issues

@maekawataiki maekawataiki added the auto-deploy 自動的に検証用 AWS アカウントにデプロイするための Label label Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for 30 days with no activity.

@github-actions github-actions Bot added the stale Issues or Pull Requests with no updates label Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-deploy 自動的に検証用 AWS アカウントにデプロイするための Label stale Issues or Pull Requests with no updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant