Skip to content

feat(api,framework): replace ctx.update with replyHandle.edit#10773

Merged
ChmaraX merged 5 commits intonextfrom
feat/agent-ctx-reply-handle
Apr 17, 2026
Merged

feat(api,framework): replace ctx.update with replyHandle.edit#10773
ChmaraX merged 5 commits intonextfrom
feat/agent-ctx-reply-handle

Conversation

@ChmaraX
Copy link
Copy Markdown
Contributor

@ChmaraX ChmaraX commented Apr 17, 2026

Summary

  • ctx.updatereplyHandle.edit. ctx.update posted a new message (without signals) instead of editing the previously sent one — producing thread clutter and misleading the developer who expected "update" to mean "edit in place." Replaced with a handle-based API that mirrors the underlying chat SDK's SentMessage.edit().
  • ctx.reply returns a ReplyHandle with { messageId, platformThreadId, edit(content) }. Calling handle.edit(...) mutates the existing platform message via adapter.editMessage instead of posting a new one.
  • API wire contract: AgentReplyPayload.update replaced with edit: { messageId, content }. The API usecase now returns SentMessageInfo | null — the phantom status: 'ok' | 'edited' tag was only read by tests; the framework's response parser only ever extracted messageId/platformThreadId.
  • Persistence: edits persist as ConversationActivityTypeEnum.EDIT (renamed from UPDATE) with platformMessageId recorded.

New DX

Before:

await ctx.reply({ markdown: 'Thinking...' });
// ...later
await ctx.update({ markdown: answer }); // surprise: posts a NEW message

After:

const msg = await ctx.reply({ markdown: 'Thinking...' });
// ...later
await msg.edit({ markdown: answer }); // mutates the same platform message

msg.edit(...) mutates msg in place and returns the same handle, so chained edits (msg.edit(a); msg.edit(b);) keep working even on platforms where editMessage happens to return a refreshed id.

Flow

sequenceDiagram
  participant User as user code
  participant Ctx as AgentContextImpl
  participant API as POST /v1/agents/:id/reply
  participant Chat as chat SDK adapter

  User->>Ctx: ctx.reply(content)
  Ctx->>API: { reply: content }
  API->>Chat: thread.post(...)
  Chat-->>API: { id, threadId }
  API-->>Ctx: { messageId, platformThreadId }
  Ctx-->>User: ReplyHandle

  Note over User,Chat: later — same messageId, mutates in place
  User->>API: msg.edit(content) → { edit: { messageId, content } }
  API->>Chat: adapter.editMessage(threadId, messageId, ...)
  Chat-->>API: { id, threadId }
  API-->>User: refreshed ids (same handle)
Loading

Breaking changes

  • Framework: ctx.update(content) removed. Migration: const msg = await ctx.reply(...); await msg.edit(...). No @deprecated cycle — the agent surface is pre-GA behind IS_CONVERSATIONAL_AGENTS_ENABLED and has no external consumers.
  • Wire: AgentReplyPayload.update removed; use edit: { messageId, content }.
  • Wire response: reply/edit paths now return { messageId, platformThreadId } directly (no status wrapper); flush-only path returns null.
  • DAL: ConversationActivityTypeEnum.UPDATE ('update') → EDIT ('edit'). Pre-GA feature behind the same flag — no material prod data impact; two code consumers (usecase + e2e), zero UI/analytics consumers. Because the feature is flag-gated and unreleased, no backfill migration is shipped with this PR.
  • Validation: edit cannot be combined with reply, signals, or resolve — each is now a hard 400.

Notes for reviewers

  • edit requires the platform adapter to implement editMessage; returns 400 with a clear error if not supported.
  • The UPDATE → EDIT enum rename is deliberate: UPDATE was named after ctx.update which no longer exists.
  • SentMessageInfo lives in @novu/framework and is imported directly by apps/api — single source of truth for the wire-message-reference type.

Addressed from CodeRabbit review

  • ReplyHandleImpl.edit now mutates this and returns the same handle (matches the ReplyHandle.edit contract: "returns the same handle for chaining"). The handle's messageId/platformThreadId are refreshed from the edit response so subsequent edits target the right platform message even if an adapter ever returns a refreshed id.
  • New ConversationRepository.touchPreview primitive; deliverEdit now refreshes lastActivityAt + lastMessagePreview without incrementing messageCount. Previously the common "post placeholder, then edit with the real answer" flow left the conversation's lastMessagePreview stuck on the placeholder text. E2e asserts the new behavior and that messageCount stays unchanged.
  • Stale e2e test title ("persist an update activity" → "persist an edit activity").
  • Stale inline comment about flush responses returning {status} (they return null now).
  • PR title scope api-serviceapi (matches allowed scope list).

Intentionally not addressed

  • Migration for UPDATE → EDIT enum rename. Feature is pre-GA behind IS_CONVERSATIONAL_AGENTS_ENABLED with no external consumers — no backfill needed.
  • No @deprecated cycle for ctx.update. Same rationale: pre-GA surface, zero downstream consumers; the cycle would be ceremony.
  • getPrimaryChannel requires serializedThread even on the edit path which doesn't use it. Hypothetical in practice; any conversation you can edit already has a serializedThread. Will relax if we hit a real case.

Test plan

  • packages/framework unit tests: 26/26 pass (includes new msg.edit() cases covering payload shape and that signals/resolve are not attached to edits).
  • apps/api typecheck clean on agents/.
  • Reviewer: pnpm --filter @novu/api test:e2e agent-reply.e2e.ts — covers reply, edit, flush-only paths, plus rejection of invalid combinations.
  • Manual: verify a real Slack integration produces an in-place edit (visible as "(edited)" in Slack) rather than a duplicated message.

Made with Cursor

ctx.update sent a new message without signals instead of editing the
previously sent one, producing thread clutter and misleading DX.

Framework:
- Remove ctx.update.
- ctx.reply now returns a ReplyHandle with edit(), closing over the
  messageId/platformThreadId so subsequent edits mutate the same message.
- Export ReplyHandle, EditPayload, SentMessageInfo from the public API.

API:
- Replace update in AgentReplyPayloadDto with edit { messageId, content }.
- ChatSdkService.postToConversation now returns SentMessageInfo; add
  editInConversation calling adapter.editMessage.
- HandleAgentReply returns SentMessageInfo | null (no phantom status field);
  persists an EDIT activity with platformMessageId on the edit path.
- Reject edit combined with signals or resolve.

DAL:
- ConversationActivityRepository.createAgentActivity accepts platformMessageId.
- ConversationActivityTypeEnum: UPDATE ('update') renamed to EDIT ('edit').
  Pre-GA feature, flag-gated (IS_CONVERSATIONAL_AGENTS_ENABLED).

Made-with: Cursor
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 17, 2026

Deploy preview added

Name Link
🔨 Latest commit 407c2b3
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/69e2088b139a800009f37ae5
😎 Deploy Preview https://deploy-preview-10773.dashboard-v2.novu-staging.co
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

Hey there and thank you for opening this pull request! 👋

We require pull request titles to follow specific formatting rules and it looks like your proposed title needs to be adjusted.

Your PR title is: feat(api,framework): replace ctx.update with replyHandle.edit

Requirements:

  1. Follow the Conventional Commits specification
  2. As a team member, include Linear ticket ID at the end: fixes TICKET-ID or include it in your branch name

Expected format: feat(scope): Add fancy new feature fixes NOV-123

Details:

PR title must end with 'fixes TICKET-ID' (e.g., 'fixes NOV-123') or include ticket ID in branch name

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

Refactors agent reply/update flow: renames updateedit, adds EditPayload and ReplyHandle, changes ctx.reply() to return a ReplyHandle and surfaces SentMessageInfo (messageId/platformThreadId) across the stack. Adds chat SDK edit support and replaces UPDATE activity with EDIT.

Changes

Cohort / File(s) Summary
API Controller & DTOs
apps/api/src/app/agents/agents-webhook.controller.ts, apps/api/src/app/agents/dtos/agent-reply-payload.dto.ts
Controller now maps body.edit (not body.update). Added EditPayloadDto and replaced optional update?: ReplyContentDto with edit?: EditPayloadDto.
Usecase & Command
apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.command.ts, apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts
Command DTO uses edit?: EditPayloadDto. Usecase return type changed to `Promise<SentMessageInfo
Chat SDK
apps/api/src/app/agents/services/chat-sdk.service.ts
postToConversation() now returns SentMessageInfo. New editInConversation() added to call adapter.editMessage() and return SentMessageInfo; errors when adapter lacks edit support.
DAL: Conversation Activity
libs/dal/src/repositories/conversation-activity/conversation-activity.entity.ts, libs/dal/src/repositories/conversation-activity/conversation-activity.repository.ts
Replaced UPDATE enum value with EDIT. createAgentActivity() accepts optional platformMessageId and persists it.
Framework: Agent API & Impl
packages/framework/src/resources/agent/agent.types.ts, packages/framework/src/resources/agent/agent.context.ts, packages/framework/src/resources/agent/index.ts, packages/framework/src/index.ts
Added ReplyHandle, EditPayload, SentMessageInfo types. AgentContext.reply() returns Promise<ReplyHandle> (removed update()), AgentContextImpl returns ReplyHandleImpl and _post() now parses SentMessageInfo (or null) from responses; ReplyHandleImpl.edit() calls back into _post(...).
Tests
apps/api/src/app/agents/e2e/agent-reply.e2e.ts, packages/framework/src/resources/agent/agent.test.ts
E2E and unit tests updated to expect messageId/platformThreadId in responses. Added/updated tests for edit flow and ctx.reply().edit() behavior; adjusted stubs to return SentMessageInfo.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Ctrl as Webhook Controller
    participant UC as HandleAgentReply Usecase
    participant ChatSDK as Chat SDK Service
    participant Adapter as Platform Adapter
    participant DB as Conversation Activity DB

    Client->>Ctrl: POST /v1/agents/:id/reply (reply or edit)
    Ctrl->>UC: HandleAgentReplyCommand (reply/edit payload)
    alt reply
      UC->>ChatSDK: postToConversation(content)
      ChatSDK->>Adapter: thread.post(message)
      Adapter-->>ChatSDK: SentMessageInfo {messageId, threadId}
      ChatSDK-->>UC: SentMessageInfo
      UC->>DB: createAgentActivity(REPLY, platformMessageId, platformThreadId)
    else edit
      UC->>ChatSDK: editInConversation(platformThreadId, platformMessageId, content)
      ChatSDK->>Adapter: adapter.editMessage(...)
      Adapter-->>ChatSDK: SentMessageInfo {messageId, threadId}
      ChatSDK-->>UC: SentMessageInfo
      UC->>DB: createAgentActivity(EDIT, platformMessageId, platformThreadId)
    end
    UC-->>Ctrl: SentMessageInfo | null
    Ctrl-->>Client: { messageId?, platformThreadId? }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • scopsy
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits format with valid type 'feat', valid scopes 'api,framework', and a clear imperative description that accurately summarizes the main change: replacing ctx.update with replyHandle.edit across the API and framework layers.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
packages/framework/src/resources/agent/agent.types.ts (1)

134-154: AgentContext.update removed without a @deprecated cycle.

Per coding guidelines, symbols in packages/** should carry an @deprecated JSDoc before being removed. The prior update(content) method on AgentContext has been removed outright in this PR. The PR description calls this out as acceptable because the feature is pre-GA behind IS_CONVERSATIONAL_AGENTS_ENABLED; please make sure that rationale is captured in the package changelog / migration notes so downstream consumers of @novu/framework aren't surprised by a TS build break on upgrade.

As per coding guidelines: "Deprecate symbols with @deprecated JSDoc before removing them from packages".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/resources/agent/agent.types.ts` around lines 134 -
154, The AgentContext.update method was removed without a deprecation cycle;
either restore a deprecated stub or document the breaking change: add back a
no-op update(content) signature on the AgentContext interface annotated with
`@deprecated` JSDoc pointing to the feature flag IS_CONVERSATIONAL_AGENTS_ENABLED
and noting it will be removed, or, if you intentionally remove it now, add
explicit migration notes to the package changelog for `@novu/framework` explaining
the removal and the feature-flag rationale so downstream consumers aren’t caught
by a TS build break.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app/agents/e2e/agent-reply.e2e.ts`:
- Line 103: Test title is stale: the it(...) description says "update activity"
but the test asserts an EDIT activity; update the test description in the
it(...) call in agent-reply.e2e.ts (the spec starting with the string "should
edit a previously sent message and persist an update activity") to accurately
reflect the assertion, e.g., change "persist an update activity" to "persist an
EDIT activity" or otherwise match the asserted activity type in the test body.

In
`@apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts`:
- Around line 187-223: deliverEdit currently saves an EDIT activity but doesn’t
update the parent conversation’s lastActivityAt/lastMessagePreview like
deliverMessage does via conversationRepository.touchActivity, so edits never
update inbox preview or bump last activity; modify deliverEdit to call the
conversation touch primitive after creating the activity—either call
conversationRepository.touchActivity(...) (if it can be used without
incrementing messageCount) or add/use a new method (e.g.,
conversationRepository.touchActivityPreview or touchActivityWithoutIncrement)
that updates lastActivityAt and lastMessagePreview without incrementing
messageCount, and invoke it conditionally when the edited message is the latest
(compare sent.messageId or edit.messageId against the conversation’s current
latest platform message id stored on the conversation) using conversation._id,
channel.platform, channel.platformThreadId and textFallback to set the preview.

In
`@libs/dal/src/repositories/conversation-activity/conversation-activity.entity.ts`:
- Around line 5-11: The enum rename from UPDATE → EDIT in
ConversationActivityTypeEnum changed the persisted string value and will leave
existing records with type:'update' orphaned; add a data migration that finds
ConversationActivity records where type === 'update' and updates them to 'edit'
(or run a one-off backfill if using raw DB access), ensure the migration
references ConversationActivityTypeEnum and the string literals 'update' →
'edit' so it runs before/with deployment, and gate or document the migration in
the IS_CONVERSATIONAL_AGENTS_ENABLED rollout so production data is corrected
before clients/aggregations rely on ConversationActivityTypeEnum.EDIT.

In `@packages/framework/src/resources/agent/agent.context.ts`:
- Around line 65-80: The implementation of ReplyHandleImpl.edit currently
returns a new ReplyHandleImpl (using info.messageId/info.platformThreadId) which
contradicts the documented contract; change edit() to mutate the existing
handle: after a successful poster.post call, update this.messageId and
this.platformThreadId from the returned info (if present) and then return this
(the same ReplyHandleImpl) to preserve chaining. Also ensure the
ReplyHandle.edit JSDoc in agent.types.ts and the example in AgentContext.reply's
JSDoc describe the in-place mutation/chaining behavior so types/docs match the
implementation.
- Around line 223-225: Update the stale comment in the JSON parse catch block in
agent.context.ts to reflect current behavior: replace the line "// non-JSON
responses are tolerated (e.g. flush path returns {status} only)" with a comment
that says something like "// flush-only responses return null or an empty body;
tolerate and return null" so the catch in the response-parsing logic correctly
documents that flush returns null rather than a {status} envelope.

In `@packages/framework/src/resources/agent/agent.types.ts`:
- Around line 125-132: The JSDoc on ReplyHandle.edit claims it "Returns the same
handle for chaining" but the implementation ReplyHandleImpl.edit currently
constructs and returns a new handle from the edit response; update the contract
to match the implementation: change the JSDoc on ReplyHandle.edit to state it
returns a new ReplyHandle (new handle-per-edit semantics) and update the usage
example (the ctx.reply example) to await and reassign the returned handle (e.g.
msg = await msg.edit(...)); ensure any inline comments or examples referencing
same-handle chaining are adjusted accordingly so docs and ReplyHandleImpl.edit
remain consistent.

---

Nitpick comments:
In `@packages/framework/src/resources/agent/agent.types.ts`:
- Around line 134-154: The AgentContext.update method was removed without a
deprecation cycle; either restore a deprecated stub or document the breaking
change: add back a no-op update(content) signature on the AgentContext interface
annotated with `@deprecated` JSDoc pointing to the feature flag
IS_CONVERSATIONAL_AGENTS_ENABLED and noting it will be removed, or, if you
intentionally remove it now, add explicit migration notes to the package
changelog for `@novu/framework` explaining the removal and the feature-flag
rationale so downstream consumers aren’t caught by a TS build break.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 860b712c-13da-488a-82c3-7371d5610e09

📥 Commits

Reviewing files that changed from the base of the PR and between cee52b8 and 57534e4.

📒 Files selected for processing (13)
  • apps/api/src/app/agents/agents-webhook.controller.ts
  • apps/api/src/app/agents/dtos/agent-reply-payload.dto.ts
  • apps/api/src/app/agents/e2e/agent-reply.e2e.ts
  • apps/api/src/app/agents/services/chat-sdk.service.ts
  • apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.command.ts
  • apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts
  • libs/dal/src/repositories/conversation-activity/conversation-activity.entity.ts
  • libs/dal/src/repositories/conversation-activity/conversation-activity.repository.ts
  • packages/framework/src/index.ts
  • packages/framework/src/resources/agent/agent.context.ts
  • packages/framework/src/resources/agent/agent.test.ts
  • packages/framework/src/resources/agent/agent.types.ts
  • packages/framework/src/resources/agent/index.ts

Comment thread apps/api/src/app/agents/e2e/agent-reply.e2e.ts Outdated
Comment on lines +69 to 73
if (command.edit) {
const agentName = await this.resolveValidatedAgentNameForDelivery(command, conversation);

await this.deliverMessage(
command,
conversation,
channel,
command.update,
ConversationActivityTypeEnum.UPDATE,
agentName
);

return { status: 'update_sent' };
return this.deliverEdit(command, conversation, channel, command.edit, agentName);
}
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.

⚠️ Potential issue | 🟡 Minor

Edit path still requires serializedThread via getPrimaryChannel.

getPrimaryChannel (line 135-137) throws BadRequestException when channel.serializedThread is missing, but deliverEdit only consumes channel.platform, channel.platformThreadId, and channel._integrationId — not the serialized thread. A conversation that is missing serializedThread for whatever reason (e.g. partial backfill, reconciliation) would unnecessarily fail edits. Consider relaxing the check to a per-path requirement (only needed for deliverMessage).

Low priority — flag if this scenario is realistic in production data.

Comment on lines 5 to 11
export enum ConversationActivityTypeEnum {
MESSAGE = 'message',
/** Interim status update sent via ctx.update() */
UPDATE = 'update',
/** In-place edit of a previously sent agent message, via replyHandle.edit() */
EDIT = 'edit',
/** System-generated timeline event (e.g. workflow triggered, conversation resolved) */
SIGNAL = 'signal',
}
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.

⚠️ Potential issue | 🟠 Major

Breaking enum rename requires a data migration.

Renaming UPDATE = 'update'EDIT = 'edit' changes the persisted string value. Any pre-existing ConversationActivity documents written with type: 'update' will no longer match queries filtering by ConversationActivityTypeEnum.EDIT and will silently fall outside any type-based aggregations/UI. Even though this is pre-GA behind IS_CONVERSATIONAL_AGENTS_ENABLED, please confirm there is either (a) no production data with type: 'update', or (b) a migration/backfill updating those documents to 'edit'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/dal/src/repositories/conversation-activity/conversation-activity.entity.ts`
around lines 5 - 11, The enum rename from UPDATE → EDIT in
ConversationActivityTypeEnum changed the persisted string value and will leave
existing records with type:'update' orphaned; add a data migration that finds
ConversationActivity records where type === 'update' and updates them to 'edit'
(or run a one-off backfill if using raw DB access), ensure the migration
references ConversationActivityTypeEnum and the string literals 'update' →
'edit' so it runs before/with deployment, and gate or document the migration in
the IS_CONVERSATIONAL_AGENTS_ENABLED rollout so production data is corrected
before clients/aggregations rely on ConversationActivityTypeEnum.EDIT.

Comment thread packages/framework/src/resources/agent/agent.context.ts
Comment thread packages/framework/src/resources/agent/agent.context.ts
Comment on lines +125 to +132
export interface ReplyHandle {
/** Platform-native message id (e.g. Slack ts, Teams activityId). */
readonly messageId: string;
/** Platform-native thread id this message lives in. */
readonly platformThreadId: string;
/** Edit this message in place with new content. Returns the same handle for chaining. */
edit(content: MessageContent): Promise<ReplyHandle>;
}
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.

⚠️ Potential issue | 🟡 Minor

JSDoc "Returns the same handle for chaining" doesn't match ReplyHandleImpl.edit.

The implementation returns a new ReplyHandleImpl built from the edit response (agent.context.ts lines 79). Either update the doc to describe the new-handle-per-edit semantics (and update the ctx.reply example on line 150-152 accordingly, e.g. msg = await msg.edit('Here is the answer')), or change the impl to return this. See the paired comment on agent.context.ts for tradeoffs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/resources/agent/agent.types.ts` around lines 125 -
132, The JSDoc on ReplyHandle.edit claims it "Returns the same handle for
chaining" but the implementation ReplyHandleImpl.edit currently constructs and
returns a new handle from the edit response; update the contract to match the
implementation: change the JSDoc on ReplyHandle.edit to state it returns a new
ReplyHandle (new handle-per-edit semantics) and update the usage example (the
ctx.reply example) to await and reassign the returned handle (e.g. msg = await
msg.edit(...)); ensure any inline comments or examples referencing same-handle
chaining are adjusted accordingly so docs and ReplyHandleImpl.edit remain
consistent.

@ChmaraX ChmaraX changed the title feat(api-service,framework): replace ctx.update with replyHandle.edit feat(api,framework): replace ctx.update with replyHandle.edit Apr 17, 2026
ChmaraX added 2 commits April 17, 2026 13:06
- ReplyHandleImpl.edit now mutates in place and returns `this`, matching
  the ReplyHandle.edit JSDoc contract ("returns the same handle for
  chaining"). messageId/platformThreadId are refreshed from the edit
  response so subsequent edits target the correct platform message even
  if an adapter returns a refreshed id.
- Fix stale e2e test title ("update activity" -> "edit activity").
- Fix stale inline comment about flush responses returning {status}.

Made-with: Cursor
Previously, deliverEdit persisted an EDIT activity but never touched the
parent conversation — so a common flow (post "Thinking..." placeholder,
then edit with the real answer) left the inbox preview stuck on
"Thinking..." forever.

- Add ConversationRepository.touchPreview: updates lastActivityAt and
  lastMessagePreview WITHOUT incrementing messageCount (edits mutate an
  existing message, they don't add a new one).
- Call it from HandleAgentReply.deliverEdit alongside the EDIT activity
  write.
- Strengthen the edit e2e: assert lastMessagePreview reflects the edited
  content AND messageCount stays unchanged.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/framework/src/resources/agent/agent.context.ts (1)

206-240: Response parsing is reasonable; consider a narrow log on unexpected shapes.

The envelope handling (raw or { data }), empty-body tolerance, and field-type guards are all sensible and the updated catch comment now matches the flush→null contract. One optional enhancement: when raw is non-empty and parses successfully but the shape doesn't match (falls through to return null at line 239), consider a console.warn so a silently-malformed server response doesn't manifest downstream as the generic "did not return a message handle" error from reply()/edit(). Not blocking — current behavior is safe.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/resources/agent/agent.context.ts` around lines 206 -
240, The _post method currently tolerates non-empty/parsable responses that lack
the expected envelope shape and silently returns null; add a narrow warning log
when raw is non-empty and parsing succeeds but the resulting envelope does not
contain string messageId and platformThreadId. Inside _post (near variables raw,
parsed, envelope) after the type guards that check envelope.messageId and
envelope.platformThreadId fail, emit a concise console.warn (or use an existing
logger if available) including the raw response and a short context string
(e.g., "Agent reply parsed but missing message handle") so malformed shapes are
visible without changing the return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/framework/src/resources/agent/agent.context.ts`:
- Around line 206-240: The _post method currently tolerates non-empty/parsable
responses that lack the expected envelope shape and silently returns null; add a
narrow warning log when raw is non-empty and parsing succeeds but the resulting
envelope does not contain string messageId and platformThreadId. Inside _post
(near variables raw, parsed, envelope) after the type guards that check
envelope.messageId and envelope.platformThreadId fail, emit a concise
console.warn (or use an existing logger if available) including the raw response
and a short context string (e.g., "Agent reply parsed but missing message
handle") so malformed shapes are visible without changing the return behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0cf32c66-9f12-4e3a-927a-d15dfa448d22

📥 Commits

Reviewing files that changed from the base of the PR and between 57534e4 and 701f3e9.

📒 Files selected for processing (2)
  • apps/api/src/app/agents/e2e/agent-reply.e2e.ts
  • packages/framework/src/resources/agent/agent.context.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/app/agents/e2e/agent-reply.e2e.ts

ChmaraX added 2 commits April 17, 2026 13:13
"Inbox" is a distinct Novu product; the field we're touching is
`lastMessagePreview` on a Conversation. Reworded comments and PR body
to avoid the terminology collision. No behavior change.

Made-with: Cursor
…handle

Made-with: Cursor

# Conflicts:
#	apps/api/src/app/agents/services/chat-sdk.service.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant