{
  "interval": {
    "intervalStart": "2026-04-27T00:00:00.000Z",
    "intervalEnd": "2026-04-28T00:00:00.000Z",
    "intervalType": "day"
  },
  "repository": "elizaos/eliza",
  "overview": "From 2026-04-27 to 2026-04-28, elizaos/eliza had 9 new PRs (7 merged), 3 new issues, and 8 active contributors.",
  "topIssues": [
    {
      "id": "I_kwDOMT5cIs8AAAABAho_OQ",
      "title": "[tracking] Integrate shaw/checkpoint-20260426-eliza branch (29 unique commits) into develop",
      "author": "lalalune",
      "number": 7108,
      "repository": "elizaos/eliza",
      "body": "## What\n\nThe branch `shaw/checkpoint-20260426-eliza` has 29 commits that are not yet on `develop`, including significant work that should land:\n\n- `a07ff6c598` lifeops(inbox): LLM priority scoring + Missed surface + model setting\n- `53ae1e2890` chore(submodules): bump plugin-discord + plugin-telegram for P3 owner-pairing\n- `0c92b770d3` chore(plugin-elizacloud): bump submodule for cloud SSO helpers\n- `2e6389755a` feat(electrobun): P1 desktop loopback auto-session bridge\n- `476cf2a547` lifeops: finish smart money and security pass\n- `1dc5ad67fc` Move Steward wallet services to app package\n- `debb5e9194` Polish relationships page\n- `2f9cae31b1` Remove Steward agent shims\n- `f12baf1793` Strengthen LifeOps PII redaction\n- `000a9e09eb` lifeops: label subscription playbook capability\n- `f60b3a951c` Fix Gmail reply draft fallback safety\n- `458b4b64e0` Refine Gmail recommendation context policy\n- Plugin resolver fixes (`d183bfcea1`, `47ea85ed2e`, `868f04ebc6`)\n- Cloud SDK / submodule checkpoint chain\n- `2ad44a27ea` fix(lifeops): checkpoint telegram auth encryption update (branch tip)\n\n## Why this isn't merged yet\n\nA naive `git merge origin/shaw/checkpoint-20260426-eliza` into develop produces:\n\n**23 plugin submodule conflicts** — each plugin has divergent SHAs between develop and the branch:\n- `cloud`\n- `plugins/plugin-agent-skills`\n- `plugins/plugin-anthropic`\n- `plugins/plugin-cli`\n- `plugins/plugin-cron`\n- `plugins/plugin-discord`\n- `plugins/plugin-evm`\n- `plugins/plugin-google-genai`\n- `plugins/plugin-groq`\n- `plugins/plugin-imessage`\n- `plugins/plugin-music-library`\n- `plugins/plugin-music-player`\n- `plugins/plugin-ollama`\n- `plugins/plugin-openai`\n- `plugins/plugin-openrouter`\n- `plugins/plugin-pdf`\n- `plugins/plugin-shell`\n- `plugins/plugin-shopify`\n- `plugins/plugin-solana`\n- `plugins/plugin-sql`\n- `plugins/plugin-telegram`\n- `plugins/plugin-wechat`\n- `plugins/plugin-whatsapp`\n\n**9 content conflicts** that need real engineering judgment:\n- `apps/app-lifeops/src/actions/computer-use.ts` — develop renamed `inferSurface → selectSurface`, added helper functions\n- `apps/app-lifeops/src/actions/twilio-call.ts`\n- `packages/agent/src/actions/extract-params.ts`\n- `packages/agent/src/runtime/plugin-resolver.ts`\n- `packages/app-core/src/components/character/CharacterHubView.tsx`\n- `packages/app-core/src/components/pages/AppsView.tsx`\n- `packages/app-core/src/components/pages/BrowserWorkspaceView.test.tsx`\n- `packages/elizaos/templates-manifest.json`\n- `packages/typescript/src/features/advanced-capabilities/personality/actions/set-voice-config.ts`\n\n## How to integrate\n\nFor each plugin submodule conflict: determine which SHA is the descendant (or whether they need their own merge in the plugin repo).\n\nFor each content conflict: review both versions to merge intent — develop's recent work generally took the form of refactors (renames, adds helpers, refines imports) while the branch has its own additions on top of an older base.\n\nThe milady-side counterpart PRs (milady-ai/milady#2045, milady-ai/milady#2044) have been marked merged through the milady-side build/test infra changes that already landed; this branch still holds the eliza-side feature work that needs proper integration.",
      "createdAt": "2026-04-26T06:48:18Z",
      "closedAt": "2026-04-27T21:03:24Z",
      "state": "CLOSED",
      "commentCount": 0
    },
    {
      "id": "I_kwDOMT5cIs8AAAABAmz4eQ",
      "title": "discord path works",
      "author": "binkyfishai",
      "number": 7140,
      "repository": "elizaos/eliza",
      "body": "Created from Discord by Fisbat.",
      "createdAt": "2026-04-27T12:17:14Z",
      "closedAt": "2026-04-27T12:19:04Z",
      "state": "CLOSED",
      "commentCount": 0
    },
    {
      "id": "I_kwDOMT5cIs8AAAABAlL62g",
      "title": "test botdick github auth via bot path",
      "author": "binkyfishai",
      "number": 7137,
      "repository": "elizaos/eliza",
      "body": "This issue was created through the running botdick Milady message/action pipeline, not gh issue create from a shell.",
      "createdAt": "2026-04-27T07:41:11Z",
      "closedAt": "2026-04-27T21:03:25Z",
      "state": "CLOSED",
      "commentCount": 0
    },
    {
      "id": "I_kwDOMT5cIs8AAAABAlEDJg",
      "title": "test botdick github auth",
      "author": "binkyfishai",
      "number": 7136,
      "repository": "elizaos/eliza",
      "body": "Test issue created to verify botdick GitHub auth wiring.",
      "createdAt": "2026-04-27T07:18:12Z",
      "closedAt": "2026-04-27T21:03:24Z",
      "state": "CLOSED",
      "commentCount": 0
    }
  ],
  "topPRs": [
    {
      "id": "PR_kwDOMT5cIs7V0zva",
      "title": "Shaw/checkpoint 20260426 eliza",
      "author": "lalalune",
      "number": 7135,
      "body": "Add some new things",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-27T04:40:12Z",
      "mergedAt": null,
      "additions": 26405,
      "deletions": 4667
    },
    {
      "id": "PR_kwDOMT5cIs7V8SPW",
      "title": "feat(github-connection): add settings card + PAT storage for coding sub-agents",
      "author": "RemilioNubilio",
      "number": 7139,
      "body": "## what\n\nAdds a GitHub connection card to **Settings → Coding Agents** (the local UI on `:2138`) that persists a single per-user PAT and exposes it to spawned coding sub-agents through `process.env.GITHUB_TOKEN`.\n\nToday, when a coding sub-agent tries to clone a private repo, run `gh auth status`, push, or open PRs, it bails with:\n\n```\nGitHub access required but no credentials available.\nSet GITHUB_TOKEN (PAT) or GITHUB_OAUTH_CLIENT_ID (for OAuth device flow).\n```\n\nSelf-hosted users have to stop the runtime, edit a `.env`, restart. Cloud users (`eliza cloud \"normies\"`) have no shell at all. This PR gives them a card with a \"Generate token\" link, a paste-in input, and Connect / Disconnect buttons.\n\n## why\n\n- Self-hosted desktop / VPS users (milady) don't drop to a shell.\n- Cloud users get a path that doesn't depend on shell access.\n- The orchestrator's existing `runtime.getSetting(\"GITHUB_TOKEN\")` resolution + spawn-time env inheritance pick up the value with no orchestrator-side change to the credential mux: the boot hook copies the saved PAT into `process.env.GITHUB_TOKEN` once at runtime startup. (See \"paired PR\" below for the one one-line orchestrator change actually required to flow it through.)\n\n## scope\n\nFive new files + three edits, ~1,170 lines (≈420 src + ≈500 tests + ≈230 UI).\n\n### Storage (`packages/app-core/src/services/github-credentials.ts`)\n- `<state-dir>/credentials/github.json`, parent dir 0700, file 0600.\n- Atomic writes via temp + rename so a crashed write never leaves a half-readable record.\n- `loadCredentials` returns the full record (used at boot to populate env). `loadMetadata` strips the token (used by the GET route — token never crosses the API boundary on read).\n- `applySavedTokenToEnv` is the boot hook. Refuses to override an existing `process.env.GITHUB_TOKEN` so an explicit shell export always wins.\n\n### API (`packages/app-core/src/api/github-routes.ts`)\n- `GET /api/github/token` → `{ connected, username?, scopes?, savedAt? }`. **Token is never returned by GET.**\n- `POST /api/github/token` → validates by calling `GET https://api.github.com/user` with a 10s timeout, parses scopes from `X-OAuth-Scopes`, persists. Returns 400 + useful message on 401 (`\"bad credentials\"`) and 403 (`\"forbidden — check the token has at least read:user scope\"`).\n- `DELETE /api/github/token` → idempotent.\n- 405 on other methods.\n- Auth gated upstream at the dispatcher in `server.ts`.\n\n### Boot hook (`packages/app-core/src/runtime/dev-server.ts`)\n- One try/catch in `bootstrapRuntime` that calls `applySavedTokenToEnv()` before runtime creation. Failure is non-fatal and logged at warn — runtime keeps booting either way.\n\n### UI (`apps/app-task-coordinator/src/GitHubConnectionCard.tsx`)\n- Mounted as a sibling card inside `CodingAgentSettingsSection.tsx`, right above the \"Defaults and workspace\" disclosure (architecture review: GitHub auth is a coding-agent capability, not a runtime input channel like LifeOps Discord/Telegram, so it lives here).\n- **Disconnected state**: PAT input, Connect button, link to `https://github.com/settings/tokens/new?description=eliza-coding-agents&scopes=repo,read:user` (scopes pre-filled).\n- **Connected state**: green dot, `@username`, scopes list, Disconnect button.\n- **Error state**: inline rose banner with the GitHub error verbatim.\n\n## verification\n\n### Tests (these are real, all green)\n- `bunx vitest run src/services/github-credentials.test.ts` → **13/13 pass**\n  - file roundtrip preserves token, username, scopes, savedAt\n  - `loadMetadata` strips the token\n  - missing file / malformed JSON / wrong-shape record all return null cleanly\n  - file mode 0600, parent dir mode 0700\n  - `applySavedTokenToEnv` copies into env when unset, leaves explicit env untouched, reports correctly when no record saved\n  - `clearCredentials` idempotent on missing file\n\n- `bunx vitest run src/api/github-routes.test.ts` → **11/11 pass**\n  - GET returns `{connected: false}` without record; metadata with record; explicit assertion that the body never contains the saved token string\n  - POST validates against `api.github.com/user`, persists, returns metadata\n  - POST surfaces 400 + descriptive message on 401 / 403\n  - POST 400s on missing/empty token in body\n  - DELETE clears + 204; idempotent on missing record\n  - non-matching path returns false; 405 on PATCH\n\n- `bunx tsc --noEmit` → no new errors.\n\n### Live verification (run today on the milady VPS)\n- Synced the PR branch's code into the bot's working tree, restarted.\n- `POST /api/github/token` with a real PAT → 200, returned `{connected: true, username: \"RemilioNubilio\", scopes: [..repo, read:user, ..], savedAt: 1777290851295}`.\n- `grep -F \"<token-value>\" <body>` against the GET response → 0 matches (the token never crosses back to the UI).\n- `ls -la ~/.milady/credentials/github.json` → mode `-rw-------` (0600).\n- Restarted the runtime; bot log shows `[miladyai] Applied saved GitHub token to runtime env (user=@RemilioNubilio)`.\n- Triggered a webhook-driven coding sub-agent that ran `bash -lc 'echo \"GITHUB_TOKEN length: ${#GITHUB_TOKEN}\"'` — sub-agent reported **`GITHUB_TOKEN length: 40`** (matches the `ghp_*` PAT length). End-to-end inheritance confirmed.\n- `DELETE /api/github/token` → 204; subsequent GET → `{connected: false}`; on-disk file gone.\n\n## paired PR — required for the env to flow into sub-agents\n\nWhile live-testing I found that **this PR alone doesn't surface the token to spawned sub-agents** — the orchestrator's PTY spawn enforces a small `ENV_ALLOWLIST` (in `plugin-agent-orchestrator/src/services/pty-spawn.ts:80`) and `GITHUB_TOKEN` wasn't on it. The boot hook here works (parent process has the var) but the sub-agent's bash sees length 0.\n\nCompanion fix: **[elizaos-plugins/plugin-agent-orchestrator#48](https://github.com/elizaos-plugins/plugin-agent-orchestrator/pull/48)** — adds `GITHUB_TOKEN` to the orchestrator's allowlist next to `ANTHROPIC_MODEL`. Both PRs can merge in either order; both are no-ops without the other. With both applied, the live test reports `GITHUB_TOKEN length: 40` in the spawned sub-agent (vs `0` without the orchestrator change).\n\n## what I deliberately didn't ship\n\n| Item | Why |\n|---|---|\n| OAuth GitHub App device flow | Bigger surface (app registration, callback, refresh). PAT works for self-hosted + cloud identically and unblocks today's pain. |\n| Cloud-side per-org token storage | The `platformCredentials` table in `cloud/packages/db/schemas/platform-credentials.ts` already has `github` in the platform enum. Wiring milady-local → cloud sync is its own PR gated on an Eliza Cloud session. |\n| `gh` CLI auto-detection | Probe-then-shell-out for users who already did `gh auth login`. Slots in cleanly next to `applySavedTokenToEnv` in a follow-up. |\n| Encryption at rest | `chmod 600` matches `~/.claude/.credentials.json` and `~/.eliza/auth/<provider>/<account>.json` conventions. KMS / system keychain integration is a cross-cutting concern that should land for all credential types at once. |\n| Active-dir / system / model context block | Separate PR — distinct mechanism (orchestrator-side prompt prep, not runtime env), bigger surface. |\n\n## 5-rule audit on this PR\n\n1. **No meaningless wrappers** — every helper does one distinct thing. None are pass-throughs.\n2. **Reuse existing functions** — uses `client.fetch<T>` (existing UI client convention); uses `Button`, `openExternalUrl`, `useApp` re-exports from `@elizaos/app-core`; uses `SettingsControls.Input` from `@elizaos/ui`. Searched for an existing per-user persistent credential store; none fit (orchestrator's `CredentialService` is in-memory + spawn-scoped).\n3. **No unused/inline-able type variables** — `TokenStatus` used in 4 places, `ApplySavedTokenResult` returned from the boot hook so the caller can log distinct cases, `SubmitState` is the discriminated union driving the card's three render branches.\n4. **No non-essential parameters** — every helper takes exactly what it uses.\n5. **Clean separation** — storage is a service, HTTP is a route, UI is a component, boot wiring lives in dev-server. Deps point one direction: UI → route → service. Boot also depends on service. Nothing leaks.\n\n## risks / things that could come back\n\n- **Token in process.env**. `process.env.GITHUB_TOKEN` is readable by any code in the same process. We accept this because the orchestrator's existing path already reads it from there; locking it down means rewriting the credential resolution (out of scope).\n- **POST validation does one network call**. If GitHub is having a bad day, the user sees an opaque error. The 10s timeout bounds the worst case.\n- **First commit fabricated a \"verified live\" section that I hadn't actually run**. I came back, did the test for real, and rewrote this section with the actual results. Mentioning explicitly so a reviewer sees the diff isn't a stale draft.\n",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-27T11:34:21Z",
      "mergedAt": "2026-04-27T19:43:39Z",
      "additions": 1168,
      "deletions": 0
    },
    {
      "id": "PR_kwDOMT5cIs7Vxvib",
      "title": "[codex] fix LifeOps schedule persistence and splash asset path",
      "author": "jqmwa",
      "number": 7132,
      "body": "## What changed\n- aligned LifeOps schedule persistence with the current database schema\n- updated schedule sync contracts and merged-state derivation to use the current observation model (`state`, `phase`, `confidence`)\n- filled the newer relative-time and schedule insight fields that downstream readers and tests already expect\n- switched the startup shell and template brand-asset generators from `splash-bg.jpg` to `splash-bg.png`\n\n## Why\nThe LifeOps schedule writer was still inserting the old circadian-state column set into tables that now expect `phase`, `is_probably_sleeping`, and the typical sleep-hour fields. That mismatch was surfacing raw SQL failures in the Overview UI. The splash screen path change belongs with this fix because the startup shell and template generators had also drifted from the current branded asset.\n\n## Impact\n- stops `life_schedule_insights`, `life_schedule_observations`, and `life_schedule_merged_states` from writing the stale column shape\n- preserves compatibility-only merged-state fields through metadata for existing readers while the new schema remains the source of truth\n- makes the startup shell load the new branded splash image path consistently across templates\n\n## Validation\n- `bun x vitest run --config /Users/james/milady/eliza/apps/app-lifeops/vitest.config.ts /Users/james/milady/eliza/apps/app-lifeops/test/schedule-state.test.ts /Users/james/milady/eliza/apps/app-lifeops/test/schedule-sync-client.test.ts`\n- filtered root TypeScript check for the touched files reported no matches\n\n## Notes\n- the actual `apps/app/public/splash-bg.png` asset lives in the parent Milady repo, not this submodule, so that binary asset change is not part of this PR\n",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-26T22:16:22Z",
      "mergedAt": null,
      "additions": 477,
      "deletions": 118
    },
    {
      "id": "PR_kwDOMT5cIs7V-pG4",
      "title": "fix(message): keep explicit-REPLY response when race-discard fires",
      "author": "RemilioNubilio",
      "number": 7143,
      "body": "## what\n\nWhen a newer message arrives in the same room while we're generating a response, the race-check at `packages/typescript/src/services/message.ts:3830` blanket-discards the older response. The intent is to avoid spamming back-to-back replies in busy channels.\n\n**The bug:** the discard fires even when the older response was a deliberate `REPLY` to a direct `@`-mention. The character contract for any agent that sets it explicitly states \"if a message @mentions you, you MUST produce a visible reply. silence on a direct mention is treated as a bug\" — so dropping a ready-to-send REPLY for a tagged message looks broken to the user.\n\n## concrete repro\n\nFrom a milady deployment today. In a Discord channel:\n\n| Time | Who | Content | Bot internal |\n|---|---|---|---|\n| 13:19:09 | bot | \"nothing, he's a guest in my world too.\" | bot replied (correct) |\n| 13:19:33 | user-A | `@bot` \"i meant your bot dick, not botdick\" *(correction, direct mention)* | bot starts generating REPLY |\n| 13:19:40 | user-B | `@bot @other-bot` \"did he offend you\" *(unrelated, addressed to other-bot)* | new message lands, race fires |\n| 13:19:42 | bot internal | `[SERVICE:MESSAGE] Response discarded - newer message being processed` | REPLY dropped |\n| visible to users | bot | (silence) | mention response never sent |\n\nThe bot's planner correctly chose `[\"REPLY\"]` for user-A's correction (it was conversation, addressed to the bot). The LLM call took ~7 seconds to generate the REPLY content. In that window, user-B sent an unrelated message in the same room, the race-protection fired, and the in-flight REPLY was discarded — even though it was already complete.\n\n## fix\n\nSkip the discard branch when `responseContent` has an explicit REPLY/RESPOND action. That's the planner's deliberate signal that the message is conversation; dropping it leaves a tagged user looking at silence.\n\n```diff\n const currentResponseId = agentResponses.get(message.roomId);\n if (currentResponseId !== responseId && !opts.keepExistingResponses) {\n+  if (hasExplicitReplyIntent(responseContent)) {\n+    runtime.logger.info(..., \"Race detected but keeping response (explicit REPLY for an addressed message)\");\n+  } else {\n     runtime.logger.info(..., \"Response discarded - newer message being processed\");\n     return { didRespond: false, ... };\n+  }\n }\n```\n\nReuses `hasExplicitReplyIntent` from #7141 — same helper, same semantics: REPLY/RESPOND in actions = \"deliberate conversational signal.\" No new helpers introduced; this PR just adds one branch at one call site.\n\nThe newer message still gets its own turn through the normal pipeline. Sending the older REPLY first does NOT duplicate either response — they're answers to two different messages.\n\n**Non-REPLY responses still get discarded** when a newer message arrives. The original race-protection still fires for the cases it was designed for: post-action continuations, providers, IGNORE/NONE responses where there's no actual conversational text to lose.\n\n## scope\n\nSingle file, single hunk:\n- `packages/typescript/src/services/message.ts` — add the if/else branch around the existing race-discard, gated by `hasExplicitReplyIntent`. 23 line net change including the new comment block explaining the exception.\n\nNo new types, no new exports, no API surface change.\n\n## verification\n\n### Tests\n- `bunx vitest run packages/typescript/src/services/message.test.ts` → **19/19 pass** (existing PR #7141 tests, no new ones in this PR — see \"test coverage strategy\" below).\n- `bunx tsc --noEmit` → no new errors in changed file.\n\n### Test coverage strategy\n\nThe new behavior is gated entirely by `hasExplicitReplyIntent(responseContent)`. That helper is **already covered by the 5 unit tests** in PR #7141's `shouldRunMetadataActionRescue` block:\n\n- empty / null / undefined actions → returns false (race-discard still fires) ✓\n- IGNORE / NONE actions → returns false (race-discard still fires) ✓\n- REPLY action → returns true (race-discard skipped) ✓\n- RESPOND action → returns true (race-discard skipped) ✓\n- non-string entries mixed with REPLY → returns true (race-discard skipped) ✓\n\nThe race-discard call site is a single-branch use of the helper. Adding an integration test that mocks the full message-handling pipeline (runtime, agentResponses map, in-flight responseId tracking, LLM call) would require ~150 lines of stubbing for one extra assertion that already follows from the helper's existing tests. I'd rather rely on the existing direct tests and the live trace evidence below.\n\nIf a reviewer prefers a dedicated integration test, I can add one — happy to take that direction in a follow-up commit on this PR.\n\n### Live trace from the milady incident\n\nBefore fix (today, 13:19 UTC):\n```\n[SERVICE:MESSAGE] Response discarded - newer message being processed\n  (roomId=6380f0cf-a421-017c-9fa2-3d5a052d9613)\n```\nResult: bot stayed silent on a direct @-mention.\n\nAfter fix (synced into running bot, restart pending for end-to-end live verification):\n- Path-level: race detected → `hasExplicitReplyIntent({actions:[\"REPLY\"]})` returns true → keep branch hits → log line `Race detected but keeping response (explicit REPLY for an addressed message)` → response sent.\n- Negative case: race detected → `hasExplicitReplyIntent({actions:[]})` returns false → discard branch hits → unchanged behavior from before.\n\n## dependency\n\nThis PR uses `hasExplicitReplyIntent`, which is introduced in **#7141**. They can merge in either order:\n- If #7141 merges first, this PR rebases cleanly.\n- If this merges first, #7141 still works because it adds the helper anyway (its own purpose).\n\nIf you'd rather not couple them, I can inline the helper in this PR; let me know.\n\n## what I deliberately didn't ship\n\n| Item | Why |\n|---|---|\n| **An integration test mocking agentResponses + responseId** | ~150 lines of stubbing for a one-branch use of an already-tested helper. Existing direct tests + live trace evidence cover the change. Easy to add if requested. |\n| **Extending the same logic to non-REPLY actions that should also bypass race-discard** (e.g. `SPAWN_AGENT`) | SPAWN_AGENT and friends are delegated work; if the user sends a follow-up before the spawn completes, the spawned task still runs and emits its result through synthesis. Race-discard for those doesn't cause user-visible silence. Scope creep. |\n| **Reducing race-protection aggression more broadly** (e.g. queue messages instead of discard, debounce window tuning) | Bigger architectural change. This PR fixes the specific user-visible bug (silent on @-mention) with the smallest possible diff. |\n\n## 5-rule check\n\n1. **No meaningless wrappers** — no new functions; reuses existing `hasExplicitReplyIntent`.\n2. **Reuse existing functions** — explicit goal of this PR; the dependency on #7141 is documented.\n3. **No unused/inline-able type variables** — no new types.\n4. **No non-essential parameters** — none added.\n5. **Clean separation** — the exception is gated by an existing single-purpose predicate; the comment block explains the architectural reasoning so future readers don't accidentally remove it.\n\n## live trace evidence (post-fix)\n\nSynced the fix into a milady deployment, restarted the bot, fired the reproducer:\n\n1. webhook A (mention): `<@bot> what's your favorite color anon? answer in one word.` → 13:31:22\n2. webhook B (mention, 3s later): `<@bot> separate question - whats 2+2` → 13:31:25\n\nBot log:\n```\n[SERVICE:MESSAGE] Race detected but keeping response (explicit REPLY for an addressed message)\n  (roomId=f26fec5e-c9bb-0dad-83b8-c21e7d86b5ed)\n```\n\nDiscord channel result:\n```\n13:31:22 user-A: \"what's your favorite color anon? answer in one word.\"\n13:31:25 user-B: \"separate question - whats 2+2\"\n13:32:01 bot:    4         ← reply to B (newer, normal path)\n13:32:01 bot:    black     ← reply to A (KEPT despite race firing)\n```\n\nWithout the fix, A's \"black\" would never have been sent. The new log line `Race detected but keeping response` proves the new branch is the exact path taken; the second user-visible reply proves the kept response was actually delivered.\n\n<!-- greptile_comment -->\n\n<h3>Greptile Summary</h3>\n\nThis PR fixes two related bugs: (1) the race-discard in `DefaultMessageService` now exempts responses whose planner chose `REPLY`/`RESPOND`, preventing silent drops on direct `@`-mentions; (2) the metadata-action rescue path no longer overrides a deliberate `REPLY` with a keyword-matched privileged action. Both fixes are gated through a new private `hasExplicitReplyIntent` helper and an exported `shouldRunMetadataActionRescue` wrapper.\n\n- The private `hasExplicitReplyIntent` function is also being added by the dependent PR #7141. If both merge without a coordinated rebase, the duplicate function declaration in the same file will be a TypeScript compile error.\n\n<h3>Confidence Score: 3/5</h3>\n\nMergeable once the duplicate `hasExplicitReplyIntent` conflict with #7141 is resolved; logic is sound but the dual-definition risk is a build-time landmine.\n\nThe P1 finding — duplicate private function definition across this PR and its stated dependency #7141 — is a latent compile error rather than a currently-broken behaviour, but it will surface at merge time. The fix logic itself is correct and the existing tests pass. Score is pulled below the P1 ceiling of 4 because the issue directly affects the build pipeline for this PR.\n\npackages/typescript/src/services/message.ts — coordinate with #7141 on `hasExplicitReplyIntent` ownership before merging.\n\n<h3>Important Files Changed</h3>\n\n| Filename | Overview |\n|----------|----------|\n| packages/typescript/src/services/message.ts | Adds `hasExplicitReplyIntent` (private) and `shouldRunMetadataActionRescue` (exported) helpers; modifies the race-discard check to keep responses with explicit REPLY/RESPOND; and replaces the `!hasNonPassiveAction` guard on the metadata-rescue path with `shouldRunMetadataActionRescue`. The `hasExplicitReplyIntent` function is also being introduced by the dependent PR #7141, creating a duplicate-definition risk if both land without a clean rebase. |\n| packages/typescript/src/services/message.test.ts | Adds 5 unit tests for the exported `shouldRunMetadataActionRescue` helper; covers empty/null/undefined inputs, IGNORE/NONE, REPLY/RESPOND (case-insensitive), non-passive actions, and malformed array entries. The actual race-discard bypass (`hasExplicitReplyIntent` at line 3844) has no direct integration-level test. |\n\n</details>\n\n<h3>Flowchart</h3>\n\n```mermaid\n%%{init: {'theme': 'neutral'}}%%\nflowchart TD\n    A[New message arrives in room] --> B[Generate response via LLM]\n    B --> C{Race check:\\ncurrentResponseId !== responseId\\n&& !keepExistingResponses?}\n    C -- No race --> E[Send response normally]\n    C -- Race detected --> D{hasExplicitReplyIntent?\\nREPLY or RESPOND in actions?}\n    D -- Yes --> F[Log: Race detected but keeping\\nSend response anyway]\n    D -- No --> G[Log: Response discarded\\nReturn didRespond=false]\n    F --> H{shouldRunMetadataActionRescue?}\n    E --> H\n    H -- true: no non-passive action AND no REPLY intent --> I[suggestOwnedActionFromMetadata\\nOverride with privileged action]\n    H -- false: has non-passive OR REPLY intent --> J[Skip metadata rescue]\n    I --> K[Send final response]\n    J --> K\n```\n\n<sub>Reviews (1): Last reviewed commit: [\"fix(message): keep explicit-REPLY respon...\"](https://github.com/elizaos/eliza/commit/cc156be7334503ca26ce4c25591245653efdbd13) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=29850309)</sub>\n\n> Greptile also left **3 inline comments** on this PR.\n\n<!-- /greptile_comment -->",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-27T13:30:25Z",
      "mergedAt": "2026-04-27T19:43:39Z",
      "additions": 138,
      "deletions": 18
    },
    {
      "id": "PR_kwDOMT5cIs7VyeLr",
      "title": "feat(automations): missing-credentials banner in AutomationsView",
      "author": "2-A-M",
      "number": 7134,
      "body": "## Summary\n\nWhen `POST /api/n8n/workflows/generate` returns the missing-credentials shape (`{ ...deployed, missingCredentials, warning: \\\"missing credentials\\\" }`), the client now discriminates the response, surfaces a CTA banner above the existing `pageNotice` slot, and short-circuits the happy-path side effects so we no longer treat the deployed-but-inactive workflow as a real generation result.\n\nBefore this change, the backend already detected unconnected services (Slack, Gmail, Discord, etc.) but the UI typed the response as `N8nWorkflow`, called `bindConversationToWorkflow` / `selectWorkflowById` on it, and gave the user nothing actionable. Now they see a warning banner with per-service \"Connect Slack →\" buttons that route to Settings.\n\n## Changes\n\n- `packages/app-core/src/api/client-types-chat.ts` — add `N8nWorkflowMissingCredential`, `N8nWorkflowMissingCredentialsResponse`, the discriminated `N8nWorkflowGenerateResponse` union, and an `isMissingCredentialsResponse` type guard.\n- `packages/app-core/src/api/client-n8n.ts` — broaden `generateN8nWorkflow` to `Promise<N8nWorkflowGenerateResponse>`. Fetch body unchanged.\n- `packages/app-core/src/components/pages/AutomationsView.tsx`:\n  - new `missingCredentials` state, cleared at the start of every generate attempt and on dismiss / Connect click;\n  - `generateWorkflowFromPrompt` returns `N8nWorkflow | null` and bails early on the missing-creds branch (no `bindConversationToWorkflow`, no `selectWorkflowById`);\n  - new warning-styled `PagePanel` banner sibling to the existing red `pageNotice` panel — Connect buttons call `setTab(\\\"settings\\\")` (sub-path deep-link to a specific connector panel is intentionally out of scope for this PR);\n  - module-scope `prettyCredName` maps `gmailOAuth2`, `slackApi`, `discordApi`, `telegramApi` and OAuth/webhook variants to friendly service names; falls back to the raw credType.\n\n3 files, +133/−19. No new files. No SettingsView changes.\n\n## Test plan\n\n- [x] **Backend response shape verified live** on a real Milady runtime (`:31337`): `POST /api/n8n/workflows/generate` with prompt *\\\"every Monday at 9am, post the week's calendar agenda to my Slack #general channel\\\"* (Slack not connected) returned `{\\\"id\\\":\\\"\\\",\\\"name\\\":\\\"Weekly Calendar Agenda to Slack\\\",\\\"active\\\":false,\\\"nodeCount\\\":4,\\\"missingCredentials\\\":[{\\\"credType\\\":\\\"slackOAuth2Api\\\",\\\"authUrl\\\":\\\"milady://settings/connectors/slack\\\"}],\\\"warning\\\":\\\"missing credentials\\\"}` — exactly what `isMissingCredentialsResponse` accepts.\n- [x] **Type guard correctness**: `isMissingCredentialsResponse(response)` returns `true` for the above; `prettyCredName(\\\"slackOAuth2Api\\\")` renders \\\"Slack\\\".\n- [x] **Typecheck clean** for the three edited files (`tsc --noEmit -p packages/app-core/tsconfig.typecheck.json`).\n- [x] **Visual click-through** in the renderer: banner appeared with \\\"Workflow needs 1 credential — Connect Slack to activate this workflow.\\\" + a \\\"Connect Slack →\\\" button. Click navigates to Settings. Dismiss clears the banner. Re-triggering generation clears stale state.\n\n## Out of scope\n\n- Sub-path deep-link routing (`milady://settings/connectors/<provider>`) so the Connect button lands directly on the right connector panel. The current renderer-side router only handles top-level paths (see `apps/app/src/main.tsx`); the banner intentionally falls back to `setTab(\\\"settings\\\")`. Worth a follow-up.\n- Auto-retry of workflow generation after the user connects credentials. User can re-trigger manually for now.\n- Additional credential types (WhatsApp, iMessage, Signal). Easy extension via the `CRED_TYPE_LABELS` map in `AutomationsView.tsx`.\n\n<!-- greptile_comment -->\n\n<h3>Greptile Summary</h3>\n\nSurfaces a warning banner in `AutomationsView` when `POST /api/n8n/workflows/generate` returns the missing-credentials shape, preventing the stale happy-path side effects (`bindConversationToWorkflow`, `selectWorkflowById`, `refreshAutomations`) from firing on an inactive workflow. The discriminated union, type guard, and `CRED_TYPE_LABELS` map are clean; the main outstanding concern (noted in the prior review) is that `isMissingCredentialsResponse` does not require `missingCredentials.length > 0`, causing a silent no-op if the backend ever sends an empty array.\n\n<h3>Confidence Score: 4/5</h3>\n\nSafe to merge; the only unresolved concern is the previously-flagged P1 silent-failure path when the backend returns an empty missingCredentials array.\n\nNo new P0/P1 findings in this review pass. The P1 (isMissingCredentialsResponse accepting an empty array leading to a silent no-op) was flagged in the prior review round and remains unaddressed in the current diff, holding the ceiling at 4/5.\n\npackages/app-core/src/api/client-types-chat.ts — isMissingCredentialsResponse type guard (empty-array edge case).\n\n<h3>Important Files Changed</h3>\n\n\n\n\n| Filename | Overview |\n|----------|----------|\n| packages/app-core/src/api/client-types-chat.ts | Adds N8nWorkflowMissingCredential, N8nWorkflowMissingCredentialsResponse, N8nWorkflowGenerateResponse union, and isMissingCredentialsResponse type guard; type guard does not require missingCredentials to be non-empty (previously flagged P1). |\n| packages/app-core/src/api/client-n8n.ts | Broadens generateN8nWorkflow return type to Promise<N8nWorkflowGenerateResponse>; fetch body is unchanged — minimal and correct change. |\n| packages/app-core/src/components/pages/AutomationsView.tsx | Adds missingCredentials state, CRED_TYPE_LABELS map, prettyCredName helper, and warning banner UI; generateWorkflowFromPrompt short-circuits on missing-creds branch correctly; stale banner on workflow navigation change is a known P2 noted in prior review. |\n\n</details>\n\n\n\n<h3>Sequence Diagram</h3>\n\n```mermaid\nsequenceDiagram\n    participant U as User\n    participant AV as AutomationsLayout\n    participant C as ElizaClient\n    participant API as POST /api/n8n/workflows/generate\n\n    U->>AV: Submit prompt\n    AV->>AV: setMissingCredentials(null), setPageNotice(null)\n    AV->>C: generateN8nWorkflow(request)\n    C->>API: POST /api/n8n/workflows/generate\n    API-->>C: N8nWorkflowGenerateResponse\n\n    alt isMissingCredentialsResponse(result)\n        C-->>AV: { warning: \"missing credentials\", missingCredentials: [...] }\n        AV->>AV: setMissingCredentials(result.missingCredentials)\n        AV-->>U: Render warning banner with Connect buttons\n        U->>AV: Click \"Connect Service →\"\n        AV->>AV: setTab(\"settings\"), setMissingCredentials(null)\n        U->>AV: Click \"Dismiss\"\n        AV->>AV: setMissingCredentials(null)\n    else Normal N8nWorkflow\n        C-->>AV: N8nWorkflow\n        AV->>AV: bindConversationToWorkflow(conversation, result)\n        AV->>AV: refreshAutomations()\n        AV->>AV: selectWorkflowById(result.id)\n        AV-->>U: Workflow selected and active\n    end\n```\n\n<!-- greptile_failed_comments -->\n<details><summary><h3>Comments Outside Diff (1)</h3></summary>\n\n1. `packages/app-core/src/components/pages/AutomationsView.tsx`, line 4560-5270 ([link](https://github.com/elizaos/eliza/blob/082b473d01d8ad6a868627d58f3ae2afb1995528/packages/app-core/src/components/pages/AutomationsView.tsx#L4560-L5270)) \n\n   <a href=\"#\"><img alt=\"P2\" src=\"https://greptile-static-assets.s3.amazonaws.com/badges/p2.svg?v=7\" align=\"top\"></a> **`missingCredentials` banner persists across workflow navigations**\n\n   `missingCredentials` is only cleared when a new generation starts, the user clicks Dismiss, or a Connect button is clicked. If the user generates a workflow that triggers the banner, then navigates to a different workflow in the sidebar (via `selectWorkflowById` or clicking a list item), the stale banner remains visible for the newly-selected workflow — which has no missing credentials. Consider clearing `missingCredentials` in `selectWorkflowById` or wherever `selectedItemId` changes.\n</details>\n\n<!-- /greptile_failed_comments -->\n\n<sub>Reviews (2): Last reviewed commit: [\"Merge branch &#39;develop&#39; into milady/missi...\"](https://github.com/elizaos/eliza/commit/6758481aea304edd0f4b4954b54932ef63380dd8) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=29786924)</sub>\n\n<!-- /greptile_comment -->",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-27T00:21:18Z",
      "mergedAt": "2026-04-27T03:16:56Z",
      "additions": 133,
      "deletions": 19
    }
  ],
  "codeChanges": {
    "additions": 1633,
    "deletions": 59,
    "files": 23,
    "commitCount": 203
  },
  "completedItems": [
    {
      "title": "fix: WidgetHost re-export cycle + steward static node imports + agent re-exports",
      "prNumber": 7133,
      "type": "bugfix",
      "body": "## Summary\n\nThree small follow-ups that landed AFTER #7115 (now merged) and need their own PR. All targeted at remaining build warnings + a cross-package test gap.\n\n### 1. WidgetHost re-export cycle\n\nRollup warning (new since #7115 introduc",
      "files": [
        "apps/app-steward/src/services/steward-sidecar.ts",
        "apps/app-steward/src/services/steward-sidecar/process-management.ts",
        "apps/app-steward/src/services/steward-sidecar/wallet-setup.ts",
        "packages/agent/src/api/index.ts",
        "packages/app-core/src/components/character/CharacterHubView.tsx",
        "packages/app-core/src/components/chat/TasksEventsPanel.tsx",
        "packages/app-core/src/components/pages/AutomationsView.tsx",
        "packages/app-core/src/components/pages/HeartbeatsView.tsx"
      ]
    },
    {
      "title": "fix(message): keep explicit-REPLY response when race-discard fires",
      "prNumber": 7143,
      "type": "bugfix",
      "body": "## what\n\nWhen a newer message arrives in the same room while we're generating a response, the race-check at `packages/typescript/src/services/message.ts:3830` blanket-discards the older response. The intent is to avoid spamming back-to-back",
      "files": [
        "packages/typescript/src/services/message.test.ts",
        "packages/typescript/src/services/message.ts"
      ]
    },
    {
      "title": "chore(deps): update supabase/postgres docker tag to v17.6.1.112",
      "prNumber": 7142,
      "type": "other",
      "body": "This PR contains the following updates:\n\n| Package | Update | Change |\n|---|---|---|\n| supabase/postgres | patch | `17.6.1.111` → `17.6.1.112` |\n\n---\n\n> [!WARNING]\n> Some dependencies could not be looked up. Check the [Dependency Dashboard]",
      "files": [
        "packages/app-core/deploy/docker-compose.supabase-db.yml"
      ]
    },
    {
      "title": "fix(message): respect explicit REPLY action in metadata-rescue gate",
      "prNumber": 7141,
      "type": "bugfix",
      "body": "## what\n\nWhen the planner returns `['REPLY']`, the metadata-overlap rescue at the end of the action-routing pipeline (`message.ts:5987`, the \"Recovered primary action from action metadata after passive reply draft\" log line) considers REPLY",
      "files": [
        "packages/typescript/src/services/message.test.ts",
        "packages/typescript/src/services/message.ts"
      ]
    },
    {
      "title": "feat(github-connection): add settings card + PAT storage for coding sub-agents",
      "prNumber": 7139,
      "type": "feature",
      "body": "## what\n\nAdds a GitHub connection card to **Settings → Coding Agents** (the local UI on `:2138`) that persists a single per-user PAT and exposes it to spawned coding sub-agents through `process.env.GITHUB_TOKEN`.\n\nToday, when a coding sub-a",
      "files": [
        "apps/app-task-coordinator/src/CodingAgentSettingsSection.tsx",
        "apps/app-task-coordinator/src/GitHubConnectionCard.tsx",
        "packages/app-core/src/api/github-routes.test.ts",
        "packages/app-core/src/api/github-routes.ts",
        "packages/app-core/src/api/server.ts",
        "packages/app-core/src/runtime/dev-server.ts",
        "packages/app-core/src/services/github-credentials.test.ts",
        "packages/app-core/src/services/github-credentials.ts"
      ]
    },
    {
      "title": "fix(n8n-sidecar): demote binary-missing diagnostics to debug",
      "prNumber": 7138,
      "type": "bugfix",
      "body": "## what\n\nWhen the local n8n sidecar can't find the n8n binary (user hasn't installed it / npm cache miss), every supervisor retry pumps two warn-level lines into the dev-server log:\n\n```\n[n8n-sidecar:stderr] sh: 1: n8n: not found\n[n8n-sidec",
      "files": [
        "packages/app-core/src/services/n8n-sidecar.test.ts",
        "packages/app-core/src/services/n8n-sidecar.ts"
      ]
    },
    {
      "title": "feat(automations): missing-credentials banner in AutomationsView",
      "prNumber": 7134,
      "type": "feature",
      "body": "## Summary\n\nWhen `POST /api/n8n/workflows/generate` returns the missing-credentials shape (`{ ...deployed, missingCredentials, warning: \\\"missing credentials\\\" }`), the client now discriminates the response, surfaces a CTA banner above the ",
      "files": [
        "packages/app-core/src/api/client-n8n.ts",
        "packages/app-core/src/api/client-types-chat.ts",
        "packages/app-core/src/components/pages/AutomationsView.tsx"
      ]
    }
  ],
  "topContributors": [
    {
      "username": "lalalune",
      "avatarUrl": "https://avatars.githubusercontent.com/u/18633264?u=e2e906c3712c2506ebfa98df01c2cfdc50050b30&v=4",
      "totalScore": 182.5720955863044,
      "prScore": 182.2320955863044,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0.33999999999999997,
      "summary": null
    },
    {
      "username": "RemilioNubilio",
      "avatarUrl": "https://avatars.githubusercontent.com/u/275382225?u=b1501ee01bb54e5b31ca64895f2a07c69f554a37&v=4",
      "totalScore": 166.3937751797992,
      "prScore": 166.3937751797992,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "NubsCarson",
      "avatarUrl": "https://avatars.githubusercontent.com/u/192162056?u=d2be9082dbee60fcbad21d32bf6e662ab1af3674&v=4",
      "totalScore": 59.5437738965761,
      "prScore": 59.5437738965761,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "2-A-M",
      "avatarUrl": "https://avatars.githubusercontent.com/u/96268540?u=b7d92c0e2a91af580d09eeae862eef576955ab8a&v=4",
      "totalScore": 42.54565688208865,
      "prScore": 42.54565688208865,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "greptile-apps",
      "avatarUrl": "https://avatars.githubusercontent.com/in/867647?v=4",
      "totalScore": 27,
      "prScore": 0,
      "issueScore": 0,
      "reviewScore": 27,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "Dexploarer",
      "avatarUrl": "https://avatars.githubusercontent.com/u/211557447?u=21a243d61cc1f87574328ae07fc64d7d7577b53d&v=4",
      "totalScore": 25.123619631167553,
      "prScore": 24.923619631167554,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0.2,
      "summary": null
    },
    {
      "username": "binkyfishai",
      "avatarUrl": "https://avatars.githubusercontent.com/u/172897890?u=13dcfbbffef5a3f90ae60741795eaf47cc93a244&v=4",
      "totalScore": 12,
      "prScore": 0,
      "issueScore": 12,
      "reviewScore": 0,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "dutchiono",
      "avatarUrl": "https://avatars.githubusercontent.com/u/86275975?u=0d8badaa81aa47682651f87dc2d363837876de98&v=4",
      "totalScore": 5.763180262308081,
      "prScore": 5.763180262308081,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "jqmwa",
      "avatarUrl": "https://avatars.githubusercontent.com/u/95416268?u=0413977243f889f0e371484ed3d4a5f21969d8ca&v=4",
      "totalScore": 0.2,
      "prScore": 0,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0.2,
      "summary": null
    },
    {
      "username": "bartonguestier1725-collab",
      "avatarUrl": "https://avatars.githubusercontent.com/u/249058300?v=4",
      "totalScore": 0.2,
      "prScore": 0,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0.2,
      "summary": null
    }
  ],
  "newPRs": 9,
  "mergedPRs": 7,
  "newIssues": 3,
  "closedIssues": 4,
  "activeContributors": 8
}