{
  "interval": {
    "intervalStart": "2026-04-11T00:00:00.000Z",
    "intervalEnd": "2026-04-12T00:00:00.000Z",
    "intervalType": "day"
  },
  "repository": "elizaos/eliza",
  "overview": "From 2026-04-11 to 2026-04-12, elizaos/eliza had 1 new PRs (0 merged), 0 new issues, and 2 active contributors.",
  "topIssues": [],
  "topPRs": [
    {
      "id": "PR_kwDOMT5cIs7RmT3Q",
      "title": "fix: dedupe identical action invocations + defer simple-mode reply",
      "author": "NubsCarson",
      "number": 6719,
      "body": "# Risks\n\nLow. Both changes are scoped to the message service / runtime action dispatch path. They make existing behavior more consistent rather than introducing new pathways. Existing single-action turns and simple REPLY responses are unaffected.\n\n# Background\n\n## What does this PR do?\n\nTwo related fixes to the action dispatch + reply emit path in \\`packages/typescript/src\\`. Both surfaced from real production usage where a chat-LLM-driven agent runs multiple actions per turn and the user sees duplicated or contradictory replies.\n\n### 1. \\`runtime.processActions\\`: dedupe identical action invocations within a single turn\n\nWhen the planner emits the same action twice in one response (e.g. \\`actions: [\"GMAIL_ACTION\", \"CALENDAR_ACTION\", \"CALENDAR_ACTION\"]\\` because the user message had multiple sub-intents the LLM couldn't split into per-action params), the runtime currently runs the action twice with the same params bucket. The second invocation produces identical output, which the discord plugin's callback dedup layer flags as a real bug.\n\nFix: track \\`(action.name, JSON.stringify(options.parameters))\\` keys in an \\`executedActionKeys\\` Set per processActions call, skip the repeat with a debug log. Two identical action+params runs in a single turn is never useful.\n\n### 2. \\`services/message.ts\\`: defer simple-mode reply emit until reflection has had a chance to override\n\nWhen the chat LLM picks REPLY mode with a chatty text (e.g. \"I think calendar is broken, but I could try X or Y\") and the reflection evaluator subsequently marks the task as incomplete, the existing \\`runReflectionTaskContinuation\\` path runs another LLM turn that produces a real action result. The user sees two messages: the chatty preamble AND the action result that supersedes it.\n\nFix: hold simple-mode \\`callback(responseContent)\\` in a \\`pendingSimpleEmit\\` slot, declared at \\`handleMessage\\` scope. Flush it at the end of the function, but **drop it** if reflection produced a continuation that already emitted its own response. Net effect: a chatty REPLY that gets corrected by reflection produces only the corrected message, not both.\n\nAlso extends the existing \\`suppressesPostActionContinuation\\` check to gate the reflection-continuation entry point too. Previously an action with that flag could still get re-fired by reflection's processActions invocation, defeating the flag's contract (\"stop after this action, don't run any continuation LLM turn\").\n\n## What kind of change is this?\n\nBug fixes. Two non-breaking improvements that tighten existing semantics around action callbacks and reply emission.\n\n# Documentation changes needed?\n\nNo. The behavioral changes preserve the existing public contracts:\n- \\`runtime.processActions\\` still calls handlers with the same args and returns the same shape\n- \\`Action.suppressPostActionContinuation\\` semantics are now actually honored in both continuation paths (the docstring already says \"stop after executing this action instead of running a post-action continuation LLM turn\")\n- \\`HandlerCallback\\` invocation order is unchanged for any single-action or true-multi-action turn\n\n# Testing\n\n## Where should a reviewer start?\n\n\\`packages/typescript/src/runtime.ts\\` line ~1746 (the new \\`executedActionKeys\\` Set declaration) and the dedupe check that follows the action resolution.\n\n\\`packages/typescript/src/services/message.ts\\` line ~1885 (the \\`pendingSimpleEmit\\` declaration at \\`handleMessage\\` scope), then line ~2000+ where the simple-mode branch sets it instead of calling callback directly, then the bottom of \\`handleMessage\\` where it's flushed if not overridden by reflection.\n\n## Detailed testing steps\n\nVerified end-to-end in a downstream consumer (a self-hosted milady-style bot wired to discord, gmail, and google calendar) under real user usage:\n\n- Multi-intent turn that previously produced duplicate identical callbacks: the planner emitted \\`[\"GMAIL_ACTION\", \"CALENDAR_ACTION\", \"CALENDAR_ACTION\"]\\` for \"check my inbox then create an event for april 15 2027\". Before this PR: 3 visible callbacks (2 calendar runs with identical text → discord plugin throws duplicate-callback error). After: 2 visible callbacks, no errors.\n\n- Chatty REPLY that gets reflection-corrected: the chat LLM picks REPLY with \"I think calendar is broken, you'd need to...\" after seeing prior failures in conversation memory, then reflection marks the task incomplete and re-runs as CALENDAR_ACTION which actually succeeds. Before: user sees both the chatty REPLY and the action result, contradicting each other. After: user sees only the action result.\n\n- \\`Action.suppressPostActionContinuation\\` flag now actually suppresses both \\`runPostActionContinuation\\` AND \\`runReflectionTaskContinuation\\` for actions that set it.\n\n- All existing message-service tests pass (\\`vitest run packages/typescript/src/__tests__/message-service.test.ts\\`).\n\n- Single-action turns and simple REPLY-only turns are unchanged: \\`pendingSimpleEmit\\` flushes normally if reflection doesn't run or doesn't override, and the dedupe Set is empty for single-action runs.\n\n<!-- greptile_comment -->\n\n<h3>Greptile Summary</h3>\n\nThis PR adds two behavioral fixes to the message/action dispatch path: a Set-based deduplication of identical action invocations within a single `processActions` call (in `runtime.ts`), and a deferred-emit pattern for simple-mode REPLY responses that lets the reflection evaluator override them before they are delivered via callback (in `message.ts`).\n\n- **Memory persisted before drop decision**: in the simple-mode path the response memory is written to DB via `createMemory` (line 1987) and `emitMessageSent` fires (line 1989) *before* `pendingSimpleEmit` is set. When reflection later sets `pendingSimpleEmit = null`, the chatty REPLY is already in the database; future LLM turns will include it in context as if it was delivered, silently corrupting the agent's self-model of what the user actually saw.\n\n<h3>Confidence Score: 4/5</h3>\n\nMostly safe to merge, but the simple-mode memory-persistence-before-drop issue is a correctness concern that should be addressed.\n\nThe dedup fix in runtime.ts is clean and well-scoped. The deferred-emit approach in message.ts solves the duplicate-message UX problem, but the memory and MESSAGE_SENT event for the simple-mode reply are committed to the DB before the drop decision is made. In the reflection-override scenario, the chatty REPLY sits in the DB as a sent message the user never saw, which will silently pollute the LLM's future conversation context. This is a P1 correctness issue that warrants a small follow-up fix before the PR is widely deployed.\n\npackages/typescript/src/services/message.ts — specifically the ordering of createMemory/emitMessageSent relative to the pendingSimpleEmit drop at line 2230.\n\n<h3>Important Files Changed</h3>\n\n\n\n\n| Filename | Overview |\n|----------|----------|\n| packages/typescript/src/runtime.ts | Adds `executedActionKeys` Set to deduplicate identical (action name + serialized params) invocations within a single `processActions` call; logic is correctly placed after parameter validation and scoped per-invocation. |\n| packages/typescript/src/services/message.ts | Introduces `pendingSimpleEmit` to defer simple-mode callback until after reflection; however, `createMemory` + `emitMessageSent` fire before the defer/drop decision, leaving orphaned DB memory entries when reflection overrides the simple reply. |\n\n</details>\n\n\n\n<h3>Sequence Diagram</h3>\n\n```mermaid\nsequenceDiagram\n    participant MS as message.ts handleMessage\n    participant RT as runtime.ts processActions\n    participant DB as Database\n    participant CB as Callback (UI)\n\n    Note over MS: mode = \"simple\"\n    MS->>DB: createMemory(simpleReply) ← runs before dedup decision\n    MS->>MS: emitMessageSent() ← event fires\n    MS->>MS: pendingSimpleEmit = responseContent\n\n    MS->>MS: runEvaluate() → reflection marks task incomplete\n\n    alt reflection produces continuation\n        MS->>RT: runReflectionTaskContinuation()\n        RT->>DB: createMemory(actionResult)\n        RT->>CB: callback(actionResult) → user sees action result\n        MS->>MS: pendingSimpleEmit = null\n        Note over DB: simpleReply in DB but user never saw it\n    else no continuation\n        MS->>CB: callback(pendingSimpleEmit) → user sees simple reply\n    end\n```\n\n<!-- greptile_failed_comments -->\n<details><summary><h3>Comments Outside Diff (2)</h3></summary>\n\n1. `packages/typescript/src/services/message.ts`, line 1976-1994 ([link](https://github.com/elizaos/eliza/blob/c21048b887512c1de1819bfff796a0e9e48b406e/packages/typescript/src/services/message.ts#L1976-L1994)) \n\n   <a href=\"#\"><img alt=\"P1\" src=\"https://greptile-static-assets.s3.amazonaws.com/badges/p1.svg?v=7\" align=\"top\"></a> **Simple-mode memory saved before drop decision**\n\n   `createMemory` and `emitMessageSent` both execute here — before `pendingSimpleEmit` is set. When reflection later sets `pendingSimpleEmit = null` at line 2230, the chatty REPLY is already persisted to the DB and its `MESSAGE_SENT` event has already fired. Future turns then include that response in the LLM's conversation context as if it was delivered, silently corrupting the agent's self-model of what the user actually saw.\n\n   To fix this, either move the simple-mode `createMemory` / `emitMessageSent` calls to after the reflection gate (alongside the `pendingSimpleEmit` flush), or delete the orphaned memory entry when `pendingSimpleEmit` is set to `null`:\n\n   ```typescript\n   // After line 2230 where pendingSimpleEmit is set null:\n   if (pendingSimpleEmit === null && responseMessages.length > 0) {\n       // Remove the simple-mode memory that was persisted before reflection ran\n       for (const mem of responseMessages) {\n           await runtime.deleteMemory(mem.id);\n       }\n   }\n   ```\n\n\n2. `packages/typescript/src/services/message.ts`, line 2216-2231 ([link](https://github.com/elizaos/eliza/blob/c21048b887512c1de1819bfff796a0e9e48b406e/packages/typescript/src/services/message.ts#L2216-L2231)) \n\n   <a href=\"#\"><img alt=\"P2\" src=\"https://greptile-static-assets.s3.amazonaws.com/badges/p2.svg?v=7\" align=\"top\"></a> **`pendingSimpleEmit` dropped only when `continuation.responseContent` is non-null**\n\n   `pendingSimpleEmit` is set to `null` only when `continuation.responseContent` is truthy. If `runReflectionTaskContinuation` returns `responseMessages` but `responseContent` is `null` (e.g. the continuation ran actions that set action-results but the helper returned early at line 2994–3001), the messages array is updated but `pendingSimpleEmit` is NOT cleared — so the chatty REPLY is still flushed to the user on top of whatever the actions emitted.\n\n</details>\n\n<!-- /greptile_failed_comments -->\n\n<sub>Reviews (1): Last reviewed commit: [\"fix: dedupe identical action invocations...\"](https://github.com/elizaos/eliza/commit/c21048b887512c1de1819bfff796a0e9e48b406e) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=28066268)</sub>\n\n> Greptile also left **1 inline comment** on this PR.\n\n<!-- /greptile_comment -->",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-11T00:40:45Z",
      "mergedAt": null,
      "additions": 65,
      "deletions": 2
    }
  ],
  "codeChanges": {
    "additions": 0,
    "deletions": 0,
    "files": 0,
    "commitCount": 1
  },
  "completedItems": [],
  "topContributors": [
    {
      "username": "NubsCarson",
      "avatarUrl": "https://avatars.githubusercontent.com/u/192162056?u=d2be9082dbee60fcbad21d32bf6e662ab1af3674&v=4",
      "totalScore": 15.818793079863193,
      "prScore": 15.818793079863193,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0,
      "summary": "NubsCarson: Focused on improving system reliability and action handling, opening PRs to address discord owner configuration in elizaos-plugins/plugin-discord#47 and deduplicate action invocations in elizaos/eliza#6719. These contributions reflect a primary focus on refining plugin functionality and core execution logic."
    },
    {
      "username": "greptile-apps",
      "avatarUrl": "https://avatars.githubusercontent.com/in/867647?v=4",
      "totalScore": 4.5,
      "prScore": 0,
      "issueScore": 0,
      "reviewScore": 4.5,
      "commentScore": 0,
      "summary": "greptile-apps: No activity today."
    },
    {
      "username": "odilitime",
      "avatarUrl": "https://avatars.githubusercontent.com/u/16395496?u=c9bac48e632aae594a0d85aaf9e9c9c69b674d8b&v=4",
      "totalScore": 0.2,
      "prScore": 0,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0.2,
      "summary": null
    },
    {
      "username": "jonathanbulkeley",
      "avatarUrl": "https://avatars.githubusercontent.com/u/258885064?v=4",
      "totalScore": 0.2,
      "prScore": 0,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0.2,
      "summary": null
    }
  ],
  "newPRs": 1,
  "mergedPRs": 0,
  "newIssues": 0,
  "closedIssues": 0,
  "activeContributors": 2
}