{
  "interval": {
    "intervalStart": "2026-04-28T00:00:00.000Z",
    "intervalEnd": "2026-04-29T00:00:00.000Z",
    "intervalType": "day"
  },
  "repository": "elizaos/eliza",
  "overview": "From 2026-04-28 to 2026-04-29, elizaos/eliza had 24 new PRs (0 merged), 0 new issues, and 8 active contributors.",
  "topIssues": [],
  "topPRs": [
    {
      "id": "PR_kwDOMT5cIs7Whalw",
      "title": "feat(agent): runtime operations manager + widget host refresh",
      "author": "Dexploarer",
      "number": 7166,
      "body": "## Summary\n\nBundles in-flight feature work on `feat/widget-host-cycle-and-greenup` so it isn't sitting as uncommitted state. Includes:\n\n- New `packages/agent/src/runtime/operations/` module — `RuntimeOperationManager` as the single-flight gate for provider switches / restarts / reloads, plus classifier (idempotency-key dedupe), health predicates, hot-reload + cold-restart strategies. Tests included.\n- Provider switch route now reads `Idempotency-Key`, routes through the manager rather than the legacy `providerSwitchInProgress` boolean.\n- Restart, server, and dev-platform paths refactored against the new manager.\n- Earlier widget-host cycle/chunking + steward static-import fixes (already on the branch).\n- Splash asset + launchpad fixes (already on the branch).\n\nWIP — squash / split as makes sense for review. The runtime-operations module is the largest new surface and the most reviewable as a single unit.\n\n## Test plan\n\n- [ ] `bun run test` — exercises classifier + health unit tests\n- [ ] Manual provider switch under load with idempotency key — verify dedupe\n- [ ] Hot reload path — verify health gate prevents premature traffic resume\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\n<!-- greptile_comment -->\n\n<details><summary><h3>Greptile Summary</h3></summary>\n\nThis PR introduces a `RuntimeOperationManager` module as the single-flight gate for provider switches and runtime restarts, replacing the previous `providerSwitchInProgress` boolean with a filesystem-backed operation repository, idempotency-key dedup, tier-classified reload strategies (hot/warm/cold), and health-gated promotion. Three issues need attention before merging:\n\n- Config is persisted to disk before the manager's rejection check — a `409 rejected-busy` response leaves the on-disk config mutated with the new provider while the running runtime is unchanged.\n- The `warm` tier strategy is never registered in `server.ts`, so same-family provider switches (classified as `warm` by the classifier) always fail internally with `\\\"no-strategy-for-tier\\\"`.\n- The raw API key is serialized into the persisted `ProviderSwitchIntent` JSON on disk.\n</details>\n\n<h3>Confidence Score: 3/5</h3>\n\nNot safe to merge — config-mutation-before-rejection and the missing warm strategy are current defects on the provider switch hot path.\n\nTwo P1 behavioral bugs (config written before rejection check, warm strategy unregistered causing silent failures) plus one P1 security issue (API key on disk) pull the score below the P1 ceiling of 4. Multiple P1s in core paths warrant a 3.\n\npackages/agent/src/api/provider-switch-routes.ts and packages/agent/src/api/server.ts\n\n<details open><summary><h3>Security Review</h3></summary>\n\n- **Plaintext credential storage** (`packages/agent/src/api/provider-switch-routes.ts`, `packages/agent/src/runtime/operations/repository.ts`): The `ProviderSwitchIntent` includes the raw `apiKey` and is serialized in full to `<stateDir>/runtime-operations/<id>.json`. Although the file is created with mode `0600`, the API key is readable by any process running as the same user and persists for up to 24 hours. API keys should be stripped from the persisted record.\n</details>\n\n<details><summary><h3>Important Files Changed</h3></summary>\n\n| Filename | Overview |\n|----------|----------|\n| packages/agent/src/runtime/operations/manager.ts | New DefaultRuntimeOperationManager: single-flight gate with idempotency-key dedup, async execution chain, and health-gated promotion — logic is sound but warm-tier strategy gap causes silent failures for same-family switches |\n| packages/agent/src/runtime/operations/types.ts | Well-typed contracts for operations, phases, repository, health checks, and strategies; clean discriminated union intent model |\n| packages/agent/src/runtime/operations/classifier.ts | Pure tier classifier: returns \"warm\" for same-family switches but no warm strategy is wired in server.ts, making those operations always fail |\n| packages/agent/src/runtime/operations/repository.ts | Filesystem-backed repo with atomic writes and in-memory O(1) cache; abandoned-op reaping on hydrate is solid; file mode 0600 is appropriate but intent JSON (including API keys) still lands on disk in cleartext |\n| packages/agent/src/api/provider-switch-routes.ts | Route correctly routes through the new manager, but saves config to disk before checking the manager outcome (config mutated even on 409) and embeds the raw API key in the persisted intent |\n| packages/agent/src/api/server.ts | Manager wiring looks correct; warm strategy is missing from the strategies map causing all same-family provider switches to fail with \"no-strategy-for-tier\" |\n| packages/agent/src/runtime/operations/cold-strategy.ts | Cold restart delegates correctly to the injected restartRuntime closure, but double-appends \"shutdown-old\" phase producing a duplicate entry in the log |\n| packages/agent/src/runtime/operations/reload-hot.ts | Hot strategy correctly applies env vars and best-effort notifies plugins; defaultApplyProviderEnv double-writes config since the route also writes before submitting the operation |\n| packages/agent/src/runtime/operations/health.ts | HealthChecker with parallel execution, per-check timeouts via Promise.race, and clean required/optional semantics — well implemented |\n| packages/agent/src/runtime/operations/index.ts | Clean barrel export for the operations module |\n\n</details>\n\n</details>\n\n<details><summary><h3>Sequence Diagram</h3></summary>\n\n```mermaid\nsequenceDiagram\n    participant C as Client\n    participant R as ProviderSwitchRoute\n    participant M as RuntimeOperationManager\n    participant Repo as FilesystemRepository\n    participant S as ReloadStrategy (hot/cold)\n    participant H as HealthChecker\n\n    C->>R: POST /api/provider/switch\n    R->>R: saveElizaConfig ⚠️ written before rejection check\n    R->>M: start({intent, idempotencyKey})\n    M->>Repo: findByIdempotencyKey(key)\n    alt key exists\n        Repo-->>M: existing op\n        M-->>R: deduped\n        R-->>C: 200\n    end\n    M->>Repo: findActive()\n    alt op in flight\n        Repo-->>M: active op\n        M-->>R: rejected-busy ⚠️ config already written\n        R-->>C: 409\n    end\n    M->>Repo: create(op)\n    M-->>R: accepted\n    R-->>C: 202 + operationId\n    Note over M: async execution chain\n    M->>S: apply(ctx)\n    S-->>M: newRuntime\n    M->>H: runForRuntime(newRuntime)\n    H-->>M: HealthCheckReport\n    alt ok\n        M->>Repo: update succeeded\n    else failed\n        M->>Repo: update failed\n    end\n```\n</details>\n\n<a href=\"https://app.greptile.com/ide/claude-code?prompt=Fix%20the%20following%204%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%204%0Apackages%2Fagent%2Fsrc%2Fapi%2Fprovider-switch-routes.ts%3A136-196%0A**Config%20written%20to%20disk%20before%20rejection%20is%20checked**%0A%0A%60applyOnboardingConnectionConfig%60%20and%20%60saveElizaConfig%60%20run%20at%20lines%20136%E2%80%93137%20before%20the%20operation%20manager's%20outcome%20is%20evaluated.%20When%20the%20manager%20returns%20%60%22rejected-busy%22%60%2C%20the%20route%20correctly%20returns%20409%2C%20but%20the%20config%20file%20has%20already%20been%20mutated%20on%20disk%20with%20the%20new%20provider%20settings.%20The%20system%20is%20now%20in%20a%20split%20state%3A%20the%20config%20reflects%20provider%20B%20while%20the%20running%20runtime%20still%20uses%20provider%20A%20%28and%20no%20restart%20will%20happen%29.%20On%20the%20next%20cold%20restart%20the%20new%20provider%20config%20will%20be%20loaded%2C%20potentially%20breaking%20the%20running%20agent.%0A%0AThe%20config%20mutation%20should%20only%20be%20persisted%20once%20the%20manager%20has%20accepted%20the%20request%2C%20or%20the%20hot-strategy's%20%60defaultApplyProviderEnv%60%20should%20be%20the%20sole%20writer%20%28it's%20called%20by%20the%20strategy%20itself%20on%20the%20async%20execution%20path%29.%0A%0A%23%23%23%20Issue%202%20of%204%0Apackages%2Fagent%2Fsrc%2Fapi%2Fserver.ts%3A1098-1102%0A**%60warm%60%20tier%20has%20no%20registered%20strategy%20%E2%80%94%20same-family%20provider%20switches%20always%20fail**%0A%0A%60strategies%3A%20%7B%20cold%3A%20coldStrategy%2C%20hot%3A%20hotStrategy%20%7D%60%20omits%20the%20warm%20strategy.%20The%20classifier%20in%20%60classifier.ts%60%20returns%20%60%22warm%22%60%20for%20same-family%20provider%20switches%20%28e.g.%20%60openai%60%20%E2%86%94%20%60openai-subscription%60%29.%20In%20%60manager.ts%60%20lines%20182%E2%80%93188%2C%20a%20missing%20strategy%20calls%20%60failOperation%60%20with%20%60%22no-strategy-for-tier%22%60.%20Any%20user%20switching%20between%20providers%20in%20the%20same%20family%20will%20receive%20a%20silent%20internal%20failure%20with%20no%20clear%20error%20surfaced.%0A%0AEither%20register%20a%20warm%20strategy%20here%20%28falling%20back%20to%20the%20hot%20strategy%20is%20a%20safe%20interim%20choice%29%20or%20change%20the%20classifier%20to%20collapse%20%60warm%60%20to%20%60cold%60%20until%20the%20warm%20strategy%20exists.%0A%0A%60%60%60ts%0Astrategies%3A%20%7B%20cold%3A%20coldStrategy%2C%20hot%3A%20hotStrategy%2C%20warm%3A%20hotStrategy%20%7D%2C%0A%60%60%60%0A%0A%23%23%23%20Issue%203%20of%204%0Apackages%2Fagent%2Fsrc%2Fapi%2Fprovider-switch-routes.ts%3A139-147%0A**API%20key%20written%20to%20disk%20in%20plaintext%20via%20intent%20serialization**%0A%0AThe%20%60ProviderSwitchIntent%60%20built%20here%20includes%20the%20raw%20%60apiKey%60%20value%2C%20and%20the%20%60FilesystemRuntimeOperationRepository%60%20serializes%20the%20full%20%60RuntimeOperation%60%20%28including%20%60intent%60%29%20to%20%60%3CstateDir%3E%2Fruntime-operations%2F%3Cid%3E.json%60%20%28mode%200600%2C%20but%20still%20a%20plain%20JSON%20file%29.%20Every%20provider%20switch%20stores%20the%20user's%20API%20key%20on%20the%20filesystem%20in%20cleartext%20for%20up%20to%20the%2024-hour%20retention%20window.%0A%0AThe%20API%20key%20should%20be%20redacted%20from%20the%20persisted%20intent.%20One%20approach%20is%20to%20store%20only%20a%20boolean%20flag%20%60apiKeyProvided%3A%20true%60%20in%20the%20persisted%20intent%20while%20keeping%20the%20real%20key%20in-memory%20only%20for%20the%20duration%20of%20the%20operation.%0A%0A%23%23%23%20Issue%204%20of%204%0Apackages%2Fagent%2Fsrc%2Fruntime%2Foperations%2Fcold-strategy.ts%3A29-42%0A**%22shutdown-old%22%20phase%20is%20appended%20twice%20instead%20of%20updated%20once**%0A%0A%60ctx.reportPhase%60%20maps%20to%20%60repository.appendPhase%60%2C%20which%20always%20adds%20a%20new%20entry%20to%20the%20phases%20array.%20Calling%20it%20first%20with%20%60status%3A%20%22running%22%60%20and%20then%20immediately%20with%20%60status%3A%20%22succeeded%22%60%20produces%20two%20separate%20%60%22shutdown-old%22%60%20entries%20in%20the%20log%20rather%20than%20one%20entry%20whose%20status%20transitions.%20No%20actual%20shutdown%20work%20occurs%20between%20the%20two%20calls%20%E2%80%94%20%60restartRuntime%60%20is%20invoked%20in%20the%20%60%22start-new%22%60%20phase%20below.%0A%0AThe%20manager's%20health-check%20code%20uses%20%60appendPhase%60%20%28for%20%60%22running%22%60%29%20%2B%20%60updateLastPhase%60%20%28for%20the%20terminal%20state%29.%20The%20cold%20strategy%20should%20follow%20the%20same%20pattern%2C%20or%20both%20phases%20should%20be%20a%20single%20append%20recording%20the%20final%20status%20since%20shutdown%20is%20effectively%20instantaneous%20here.%0A%0A&repo=elizaos%2Feliza&pr=7166&platform=github\"><picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaudeDark.svg?v=2\"><source media=\"(prefers-color-scheme: light)\" srcset=\"https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaude.svg?v=2\"><img alt=\"Fix All in Claude Code\" src=\"https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaude.svg?v=2\" height=\"20\"></picture></a> <a href=\"https://chatgpt.com/codex/deeplink?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22elizaos%2Feliza%22%20on%20the%20existing%20branch%20%22feat%2Fwidget-host-cycle-and-greenup%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22feat%2Fwidget-host-cycle-and-greenup%22.%0A%0AFix%20the%20following%204%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%204%0Apackages%2Fagent%2Fsrc%2Fapi%2Fprovider-switch-routes.ts%3A136-196%0A**Config%20written%20to%20disk%20before%20rejection%20is%20checked**%0A%0A%60applyOnboardingConnectionConfig%60%20and%20%60saveElizaConfig%60%20run%20at%20lines%20136%E2%80%93137%20before%20the%20operation%20manager's%20outcome%20is%20evaluated.%20When%20the%20manager%20returns%20%60%22rejected-busy%22%60%2C%20the%20route%20correctly%20returns%20409%2C%20but%20the%20config%20file%20has%20already%20been%20mutated%20on%20disk%20with%20the%20new%20provider%20settings.%20The%20system%20is%20now%20in%20a%20split%20state%3A%20the%20config%20reflects%20provider%20B%20while%20the%20running%20runtime%20still%20uses%20provider%20A%20%28and%20no%20restart%20will%20happen%29.%20On%20the%20next%20cold%20restart%20the%20new%20provider%20config%20will%20be%20loaded%2C%20potentially%20breaking%20the%20running%20agent.%0A%0AThe%20config%20mutation%20should%20only%20be%20persisted%20once%20the%20manager%20has%20accepted%20the%20request%2C%20or%20the%20hot-strategy's%20%60defaultApplyProviderEnv%60%20should%20be%20the%20sole%20writer%20%28it's%20called%20by%20the%20strategy%20itself%20on%20the%20async%20execution%20path%29.%0A%0A%23%23%23%20Issue%202%20of%204%0Apackages%2Fagent%2Fsrc%2Fapi%2Fserver.ts%3A1098-1102%0A**%60warm%60%20tier%20has%20no%20registered%20strategy%20%E2%80%94%20same-family%20provider%20switches%20always%20fail**%0A%0A%60strategies%3A%20%7B%20cold%3A%20coldStrategy%2C%20hot%3A%20hotStrategy%20%7D%60%20omits%20the%20warm%20strategy.%20The%20classifier%20in%20%60classifier.ts%60%20returns%20%60%22warm%22%60%20for%20same-family%20provider%20switches%20%28e.g.%20%60openai%60%20%E2%86%94%20%60openai-subscription%60%29.%20In%20%60manager.ts%60%20lines%20182%E2%80%93188%2C%20a%20missing%20strategy%20calls%20%60failOperation%60%20with%20%60%22no-strategy-for-tier%22%60.%20Any%20user%20switching%20between%20providers%20in%20the%20same%20family%20will%20receive%20a%20silent%20internal%20failure%20with%20no%20clear%20error%20surfaced.%0A%0AEither%20register%20a%20warm%20strategy%20here%20%28falling%20back%20to%20the%20hot%20strategy%20is%20a%20safe%20interim%20choice%29%20or%20change%20the%20classifier%20to%20collapse%20%60warm%60%20to%20%60cold%60%20until%20the%20warm%20strategy%20exists.%0A%0A%60%60%60ts%0Astrategies%3A%20%7B%20cold%3A%20coldStrategy%2C%20hot%3A%20hotStrategy%2C%20warm%3A%20hotStrategy%20%7D%2C%0A%60%60%60%0A%0A%23%23%23%20Issue%203%20of%204%0Apackages%2Fagent%2Fsrc%2Fapi%2Fprovider-switch-routes.ts%3A139-147%0A**API%20key%20written%20to%20disk%20in%20plaintext%20via%20intent%20serialization**%0A%0AThe%20%60ProviderSwitchIntent%60%20built%20here%20includes%20the%20raw%20%60apiKey%60%20value%2C%20and%20the%20%60FilesystemRuntimeOperationRepository%60%20serializes%20the%20full%20%60RuntimeOperation%60%20%28including%20%60intent%60%29%20to%20%60%3CstateDir%3E%2Fruntime-operations%2F%3Cid%3E.json%60%20%28mode%200600%2C%20but%20still%20a%20plain%20JSON%20file%29.%20Every%20provider%20switch%20stores%20the%20user's%20API%20key%20on%20the%20filesystem%20in%20cleartext%20for%20up%20to%20the%2024-hour%20retention%20window.%0A%0AThe%20API%20key%20should%20be%20redacted%20from%20the%20persisted%20intent.%20One%20approach%20is%20to%20store%20only%20a%20boolean%20flag%20%60apiKeyProvided%3A%20true%60%20in%20the%20persisted%20intent%20while%20keeping%20the%20real%20key%20in-memory%20only%20for%20the%20duration%20of%20the%20operation.%0A%0A%23%23%23%20Issue%204%20of%204%0Apackages%2Fagent%2Fsrc%2Fruntime%2Foperations%2Fcold-strategy.ts%3A29-42%0A**%22shutdown-old%22%20phase%20is%20appended%20twice%20instead%20of%20updated%20once**%0A%0A%60ctx.reportPhase%60%20maps%20to%20%60repository.appendPhase%60%2C%20which%20always%20adds%20a%20new%20entry%20to%20the%20phases%20array.%20Calling%20it%20first%20with%20%60status%3A%20%22running%22%60%20and%20then%20immediately%20with%20%60status%3A%20%22succeeded%22%60%20produces%20two%20separate%20%60%22shutdown-old%22%60%20entries%20in%20the%20log%20rather%20than%20one%20entry%20whose%20status%20transitions.%20No%20actual%20shutdown%20work%20occurs%20between%20the%20two%20calls%20%E2%80%94%20%60restartRuntime%60%20is%20invoked%20in%20the%20%60%22start-new%22%60%20phase%20below.%0A%0AThe%20manager's%20health-check%20code%20uses%20%60appendPhase%60%20%28for%20%60%22running%22%60%29%20%2B%20%60updateLastPhase%60%20%28for%20the%20terminal%20state%29.%20The%20cold%20strategy%20should%20follow%20the%20same%20pattern%2C%20or%20both%20phases%20should%20be%20a%20single%20append%20recording%20the%20final%20status%20since%20shutdown%20is%20effectively%20instantaneous%20here.%0A%0A\"><picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodexDark.svg?v=2\"><source media=\"(prefers-color-scheme: light)\" srcset=\"https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=2\"><img alt=\"Fix All in Codex\" src=\"https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=2\" height=\"20\"></picture></a> <a href=\"https://app.greptile.com/api/ide/cursor?prompt=Fix%20the%20following%204%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%204%0Apackages%2Fagent%2Fsrc%2Fapi%2Fprovider-switch-routes.ts%3A136-196%0A**Config%20written%20to%20disk%20before%20rejection%20is%20checked**%0A%0A%60applyOnboardingConnectionConfig%60%20and%20%60saveElizaConfig%60%20run%20at%20lines%20136%E2%80%93137%20before%20the%20operation%20manager's%20outcome%20is%20evaluated.%20When%20the%20manager%20returns%20%60%22rejected-busy%22%60%2C%20the%20route%20correctly%20returns%20409%2C%20but%20the%20config%20file%20has%20already%20been%20mutated%20on%20disk%20with%20the%20new%20provider%20settings.%20The%20system%20is%20now%20in%20a%20split%20state%3A%20the%20config%20reflects%20provider%20B%20while%20the%20running%20runtime%20still%20uses%20provider%20A%20%28and%20no%20restart%20will%20happen%29.%20On%20the%20next%20cold%20restart%20the%20new%20provider%20config%20will%20be%20loaded%2C%20potentially%20breaking%20the%20running%20agent.%0A%0AThe%20config%20mutation%20should%20only%20be%20persisted%20once%20the%20manager%20has%20accepted%20the%20request%2C%20or%20the%20hot-strategy's%20%60defaultApplyProviderEnv%60%20should%20be%20the%20sole%20writer%20%28it's%20called%20by%20the%20strategy%20itself%20on%20the%20async%20execution%20path%29.%0A%0A%23%23%23%20Issue%202%20of%204%0Apackages%2Fagent%2Fsrc%2Fapi%2Fserver.ts%3A1098-1102%0A**%60warm%60%20tier%20has%20no%20registered%20strategy%20%E2%80%94%20same-family%20provider%20switches%20always%20fail**%0A%0A%60strategies%3A%20%7B%20cold%3A%20coldStrategy%2C%20hot%3A%20hotStrategy%20%7D%60%20omits%20the%20warm%20strategy.%20The%20classifier%20in%20%60classifier.ts%60%20returns%20%60%22warm%22%60%20for%20same-family%20provider%20switches%20%28e.g.%20%60openai%60%20%E2%86%94%20%60openai-subscription%60%29.%20In%20%60manager.ts%60%20lines%20182%E2%80%93188%2C%20a%20missing%20strategy%20calls%20%60failOperation%60%20with%20%60%22no-strategy-for-tier%22%60.%20Any%20user%20switching%20between%20providers%20in%20the%20same%20family%20will%20receive%20a%20silent%20internal%20failure%20with%20no%20clear%20error%20surfaced.%0A%0AEither%20register%20a%20warm%20strategy%20here%20%28falling%20back%20to%20the%20hot%20strategy%20is%20a%20safe%20interim%20choice%29%20or%20change%20the%20classifier%20to%20collapse%20%60warm%60%20to%20%60cold%60%20until%20the%20warm%20strategy%20exists.%0A%0A%60%60%60ts%0Astrategies%3A%20%7B%20cold%3A%20coldStrategy%2C%20hot%3A%20hotStrategy%2C%20warm%3A%20hotStrategy%20%7D%2C%0A%60%60%60%0A%0A%23%23%23%20Issue%203%20of%204%0Apackages%2Fagent%2Fsrc%2Fapi%2Fprovider-switch-routes.ts%3A139-147%0A**API%20key%20written%20to%20disk%20in%20plaintext%20via%20intent%20serialization**%0A%0AThe%20%60ProviderSwitchIntent%60%20built%20here%20includes%20the%20raw%20%60apiKey%60%20value%2C%20and%20the%20%60FilesystemRuntimeOperationRepository%60%20serializes%20the%20full%20%60RuntimeOperation%60%20%28including%20%60intent%60%29%20to%20%60%3CstateDir%3E%2Fruntime-operations%2F%3Cid%3E.json%60%20%28mode%200600%2C%20but%20still%20a%20plain%20JSON%20file%29.%20Every%20provider%20switch%20stores%20the%20user's%20API%20key%20on%20the%20filesystem%20in%20cleartext%20for%20up%20to%20the%2024-hour%20retention%20window.%0A%0AThe%20API%20key%20should%20be%20redacted%20from%20the%20persisted%20intent.%20One%20approach%20is%20to%20store%20only%20a%20boolean%20flag%20%60apiKeyProvided%3A%20true%60%20in%20the%20persisted%20intent%20while%20keeping%20the%20real%20key%20in-memory%20only%20for%20the%20duration%20of%20the%20operation.%0A%0A%23%23%23%20Issue%204%20of%204%0Apackages%2Fagent%2Fsrc%2Fruntime%2Foperations%2Fcold-strategy.ts%3A29-42%0A**%22shutdown-old%22%20phase%20is%20appended%20twice%20instead%20of%20updated%20once**%0A%0A%60ctx.reportPhase%60%20maps%20to%20%60repository.appendPhase%60%2C%20which%20always%20adds%20a%20new%20entry%20to%20the%20phases%20array.%20Calling%20it%20first%20with%20%60status%3A%20%22running%22%60%20and%20then%20immediately%20with%20%60status%3A%20%22succeeded%22%60%20produces%20two%20separate%20%60%22shutdown-old%22%60%20entries%20in%20the%20log%20rather%20than%20one%20entry%20whose%20status%20transitions.%20No%20actual%20shutdown%20work%20occurs%20between%20the%20two%20calls%20%E2%80%94%20%60restartRuntime%60%20is%20invoked%20in%20the%20%60%22start-new%22%60%20phase%20below.%0A%0AThe%20manager's%20health-check%20code%20uses%20%60appendPhase%60%20%28for%20%60%22running%22%60%29%20%2B%20%60updateLastPhase%60%20%28for%20the%20terminal%20state%29.%20The%20cold%20strategy%20should%20follow%20the%20same%20pattern%2C%20or%20both%20phases%20should%20be%20a%20single%20append%20recording%20the%20final%20status%20since%20shutdown%20is%20effectively%20instantaneous%20here.%0A%0A&pr=7166&platform=github\"><picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursorDark.svg?v=2\"><source media=\"(prefers-color-scheme: light)\" srcset=\"https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursor.svg?v=2\"><img alt=\"Fix All in Cursor\" src=\"https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursor.svg?v=2\" height=\"20\"></picture></a>\n\n<sub>Reviews (1): Last reviewed commit: [\"feat(agent): runtime operations manager ...\"](https://github.com/elizaos/eliza/commit/3b41268180da76fa04f245d949cf9132bc08005e) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=30101058)</sub>\n\n> Greptile also left **4 inline comments** on this PR.\n\n<!-- /greptile_comment -->",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-28T23:14:38Z",
      "mergedAt": "2026-04-29T02:39:15Z",
      "additions": 6861,
      "deletions": 619
    },
    {
      "id": "PR_kwDOMT5cIs7WhgqJ",
      "title": "feat(confidant): phase 0 — credential mediation layer for Eliza agents",
      "author": "Dexploarer",
      "number": 7167,
      "body": "## Summary\n\nIntroduces `@elizaos/confidant` — the single seam at which credentials can be observed inside an Eliza agent. Skills never read `process.env` for credentials; they request from a per-skill `ScopedConfidant` that goes through policy + audit. Confidant stores values literally (AES-256-GCM at rest, secret id bound as AAD) or as references into external password managers.\n\nPhase 0 ships the contract; the runtime does not yet call into Confidant. Phase 1 wires it in.\n\n## What this fixes (concretely)\n\nSeven failure modes observable today in elizaOS-based agents:\n\n1. **Skill exfiltration** — `process.env.OPENROUTER_API_KEY` is process-global. Any plugin or skill in the runtime can read every credential. No boundary, no audit.\n2. **Dual writers** — `api/plugins-compat-routes.ts` writes `config.env[KEY]`; `api/provider-switch-config.ts setEnvValue` writes BOTH `config.env[KEY]` and `config.env.vars[KEY]`. Same value lands twice in two layouts.\n3. **Catalog as authoritative** — `Object.values(config).find(non-empty)` is used to \"find the API key\" in the save path because the persistence layer never had a real schema seam. A user typing the model field before the API-key field overwrites the API key with the model slug.\n4. **Subscription state leaks** — device-bound OAuth tokens collide with API keys in the same `milady.json` JSON blob; different lifecycles, last-write-wins file semantics.\n5. **Disconnect doesn't clean up** — disconnecting Eliza Cloud clears `cloud.apiKey` and `serviceRouting.llmText` but leaves `serviceRouting.tts/media/embeddings/rpc` pointing at a now-unauthenticated cloud-proxy.\n6. **No reveal** — once an API key is saved through `ApiKeyConfig`, the user cannot read it back to verify; bugs in the save path go silent.\n7. **No password-manager integration** — users with 1Password / Proton Pass / Bitwarden / OS-keychain entries already have their keys somewhere safe; today they must copy-paste into a plaintext JSON.\n\nPhase 0 establishes the contract that closes all seven; phases 1-6 are the migration that lands them. The full design including the 7-phase migration plan is in the Milady-side architecture doc; happy to mirror it here if maintainers prefer it landed in `eliza/docs/`.\n\n## What's in this PR\n\n**Public API**\n- `createConfidant`, `ScopedConfidant`\n- `defineSecretSchema(...)` — single source of truth for \\\"this id is a secret\\\"; replaces the catalog-as-authoritative pattern\n- `parseReference` / `buildReference` — URI scheme dispatch (`op://`, `pass://`, `keyring://`, `file://`, `env://`, `cloud://`)\n- AES-256-GCM envelope helpers\n- Identifier validation + glob-pattern matching with most-specific selection\n\n**Backends**\n- `KeyringBackend` — cross-platform via `@napi-rs/keyring` (macOS Keychain, Windows Credential Manager, Linux libsecret). Prebuilt binaries for darwin/win/linux/freebsd.\n- `EnvLegacyBackend` — read-only `env://VAR` migration scaffolding, removed in phase 6.\n- 1Password / Proton Pass / Cloud are deferred to phase 4+.\n\n**Storage**\n- `~/.milady/confidant.json` mode 0600, atomic-rename writes.\n- Master key in OS keychain by default; pluggable via `inMemoryMasterKey` for tests / headless deployments.\n- Secret id bound as AAD so a swapped ciphertext fails closed.\n\n**Policy**\n- Deny-by-default. Implicit grant for the registering plugin. Explicit grants persisted per-skill with glob patterns; most-specific wins; deny absolute. `prompt` mode requires a `PromptHandler` (UI hook) supplied at construction.\n\n**Audit log**\n- `~/.milady/audit/confidant.jsonl`, append-only JSONL. Records ids, never values. Explicit test verifies the secret value never appears in the log.\n\n## Test plan\n\n70 vitest cases:\n\n- Envelope: encrypt/decrypt round-trip, AAD binding, GCM tag tampering, key/nonce length, version handshake.\n- Identifiers + references: validation, glob matching, specificity selection, URI parsing for every scheme.\n- Store: 0600 mode, atomic write, version refusal, malformed JSON rejection, permissions round-trip.\n- Policy: deny-by-default, implicit grant, deny precedence, prompt mode, specificity.\n- End-to-end: literal set/resolve, env-legacy reference resolve, scoped permission denial, lazy resolve, prompt-handler approval caching, audit log shape, ciphertext absence on disk, audit log absence of value, concurrent-set safety.\n- Cross-platform keyring: real round-trip via `@napi-rs/keyring`. Skips cleanly on hosts without a usable Secret Service agent (probe runs at module-import time so vitest's `it.skipIf` evaluates correctly).\n\nAll 70 pass on macOS. Cross-platform CI hooks would surface any darwin-specific bug.\n\n- [ ] `bun run test` in the package\n- [ ] Cross-platform CI matrix (macOS / Windows / Linux)\n\n## Notes for reviewers\n\n- The package depends on `@napi-rs/keyring`. This is the only native dependency.\n- Six open questions documented in the design (audit retention, prompt UI, sync-metadata semantics, etc.) are deliberately deferred to phase 1.\n- This is the first piece of a multi-phase migration. The runtime does not yet read or write through Confidant; existing call sites are untouched.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-28T23:22:06Z",
      "mergedAt": null,
      "additions": 2041,
      "deletions": 1
    },
    {
      "id": "PR_kwDOMT5cIs7WZJF1",
      "title": "feat(n8n): propagate originating-conversation routing into workflow generator",
      "author": "2-A-M",
      "number": 7164,
      "body": "## Summary\n\nWhen a workflow is generated from inside a platform conversation (e.g. a Discord DM or a Telegram chat), surface the originating channel or chat to the LLM as a `## Runtime Facts` line. The user can say *\\\"post the result back to this channel\\\"* and the generated send node targets the right Discord channel ID / Telegram chat ID without having to name it — closing the long-tail of the same NL ergonomics goal that motivated #7134's missing-credentials banner.\n\n## End-to-end wiring\n\n- **`client-types-chat.ts`**: extend `N8nWorkflowGenerateRequest` with optional `bridgeConversationId`. `AutomationsView` already had the id in scope (it uses it to bind the workflow to the originating conversation); now also forwards it on the generation request.\n- **`n8n-routes.ts`**: when `bridgeConversationId` is present, read the originating conversation's tail inbound message metadata via `runtime.getMemories({ roomId, tableName: \\\"messages\\\", count: 12 })`, derive a `TriggerContext` (Discord channelId/guildId, Telegram chatId/threadId, Slack channelId/teamId), and thread it into `service.generateWorkflowDraft(prompt, { triggerContext })`. The helper reads **both** the canonical `metadata.discord.{channelId,guildId}` sub-object **and** the legacy flat `discordChannelId` / `discordServerId` fields — pre-existing schema gap (canonical wins when present, flat is the fallback so nothing today breaks).\n- **`n8n-runtime-context-provider.ts`**: extend `RuntimeContextProviderInput` to accept the trigger context, render it as a fact line:\n\n  > This workflow was prompted from a Discord conversation in #general (id 9876543210) within \\\"Cozy Devs\\\" (id 1234567890). When the user references \\\"this channel\\\" or \\\"back to here\\\", target that channel ID.\n\n  Same pattern for Telegram chats and Slack channels. Empty/missing routing data → no fact line.\n\n## Backward compatibility\n\n- Routes still work without `bridgeConversationId` (no triggerContext threading, baseline behavior).\n- Plugin still works with hosts that don't pass triggerContext (the optional `opts` arg on `generateWorkflowDraft` is unused — see elizaos-plugins/plugin-n8n-workflow#26).\n\n## Depends on\n\n- **Stacks on #7163** (n8n runtime-context provider). Until that merges, this PR's diff includes the dependency. Once #7163 merges, GitHub auto-rebases and the diff collapses to the trigger-context plumbing.\n- **Runtime depends on elizaos-plugins/plugin-n8n-workflow#26** (TriggerContext on RuntimeContextProviderInput). Host code compiles fine without the plugin upgrade; falls through cleanly until the plugin pointer bumps.\n\n## Test plan\n\n- [ ] From a Discord DM: prompt *\\\"every weekday at 9am post the day's calendar agenda back to this channel\\\"*. Inspect the deployed workflow's `Discord Send` node — `channelId` equals the originating Discord channel id, not blank, not a placeholder.\n- [ ] Same prompt from a Telegram chat → `Telegram Send` node `chatId` equals the originating Telegram chat id.\n- [ ] No `bridgeConversationId` in the request → behavior unchanged.\n\n## Out of scope (follow-up)\n\n- Persist `originChannelContext` on the workflow's conversation metadata so re-runs without a fresh inbound message still target the same channel.\n- Switch upstream plugin-discord/telegram from flat metadata fields to the canonical nested `metadata.discord.{channelId,guildId,messageId}` shape — this PR's helper handles both transitionally.\n\n<!-- greptile_comment -->\n\n<h3>Greptile Summary</h3>\n\nThis PR wires originating-conversation routing (Discord channel/guild, Telegram chat, Slack channel) into the n8n workflow generator so the LLM can resolve \"post back to this channel\" without the user supplying an ID. It also introduces the `n8n-runtime-context-provider` service (with tests) and registers it during agent boot.\n\nThe previously-flagged P1 (`fromId` used as Telegram `chatId` fallback) has been correctly addressed — the code now skips Telegram routing rather than guess. The `discordWebhookApi`/`googleOAuth2Api` inconsistency has also been resolved. Remaining notes are P2 quality items (memory ordering comment and missing `triggerContext` test coverage).\n\n<h3>Confidence Score: 5/5</h3>\n\nSafe to merge; no blocking issues remain — all P1s from the previous review have been addressed\n\nThe Telegram fromId P1 is fixed and the discordWebhookApi/googleOAuth2Api P2 is also resolved. All remaining findings are P2: a misleading memory-ordering comment, a trivially redundant inner conditional, and missing test coverage for the new triggerContext path. None of these block correctness.\n\npackages/app-core/src/api/n8n-routes.ts (memory ordering comment), packages/app-core/src/services/n8n-runtime-context-provider.test.ts (missing triggerContext tests)\n\n<h3>Important Files Changed</h3>\n\n\n\n\n| Filename | Overview |\n|----------|----------|\n| packages/app-core/src/api/n8n-routes.ts | Adds buildTriggerContextFromConversation helper and threads triggerContext into generateWorkflowDraft; the Telegram fromId P1 has been addressed but memory ordering logic has a misleading comment |\n| packages/app-core/src/services/n8n-runtime-context-provider.ts | New service file; surfaces Discord/Gmail/triggerContext facts to the n8n workflow generator; well-structured with caching and defensive fallbacks |\n| packages/app-core/src/services/n8n-runtime-context-provider.test.ts | New test file covering registration, Discord facts, Gmail facts, credential filtering, network failures, and caching — but no test cases for the new triggerContext / formatTriggerContextFact path |\n| packages/app-core/src/runtime/eliza.ts | Registers n8n runtime-context provider via ensureN8nRuntimeContextProvider; pattern mirrors existing bridge initializers; clean lifecycle management |\n| packages/app-core/src/api/client-types-chat.ts | Adds optional bridgeConversationId field to N8nWorkflowGenerateRequest; straightforward and well-documented |\n| packages/app-core/src/components/pages/AutomationsView.tsx | Forwards bridgeConversationId into the workflow generation request; minimal one-line change using existing in-scope variable |\n\n</details>\n\n\n\n<h3>Sequence Diagram</h3>\n\n```mermaid\nsequenceDiagram\n    participant Client as AutomationsView (client)\n    participant Route as n8n-routes (server)\n    participant Runtime as AgentRuntime\n    participant CtxProvider as N8nRuntimeContextProvider\n    participant Service as plugin-n8n-workflow\n\n    Client->>Route: POST /generate {prompt, bridgeConversationId}\n    Route->>Runtime: getMemories({roomId: bridgeConversationId, count: 12})\n    Runtime-->>Route: Memory[] (messages)\n    Note over Route: buildTriggerContextFromConversation()<br/>finds first non-agent message,<br/>reads metadata.discord / metadata.telegram / metadata.slack\n    Route-->>Route: TriggerContext {source, discord/telegram/slack}\n    Route->>Service: generateWorkflowDraft(prompt, {triggerContext})\n    Service->>CtxProvider: getRuntimeContext({userId, relevantNodes, triggerContext})\n    CtxProvider-->>Service: {facts: [This workflow was prompted from Discord...], supportedCredentials: [...]}\n    Service-->>Route: WorkflowDraft (with channel IDs filled in)\n    Route-->>Client: WorkflowDraft JSON\n```\n\n<!-- greptile_failed_comments -->\n<details><summary><h3>Comments Outside Diff (4)</h3></summary>\n\n1. `packages/app-core/src/api/n8n-routes.ts`, line 111-117 ([link](https://github.com/elizaos/eliza/blob/4cae1c5dfebabc071b953babb33dbd2477397a3f/packages/app-core/src/api/n8n-routes.ts#L111-L117)) \n\n   <a href=\"#\"><img alt=\"P1\" src=\"https://greptile-static-assets.s3.amazonaws.com/badges/p1.svg?v=7\" align=\"top\"></a> **`fromId` is not `chatId` in Telegram group contexts**\n\n   `meta.fromId` is the Telegram sender's *user* ID, not the chat ID. In a private DM the two happen to be the same, but in a group chat or channel the chat ID is a distinct negative integer. Using `fromId` as the fallback `chatId` will cause the generated workflow to target the individual user's DM inbox instead of the originating group, silently breaking the \"post back to this channel\" feature for the majority of Telegram team-chat scenarios.\n\n   If the canonical `metadata.telegram.chatId` isn't populated, the safest fallback is `undefined` (return no Telegram trigger context) rather than a value that may route to the wrong entity.\n\n2. `packages/app-core/src/services/n8n-runtime-context-provider.ts`, line 641-654 ([link](https://github.com/elizaos/eliza/blob/4cae1c5dfebabc071b953babb33dbd2477397a3f/packages/app-core/src/services/n8n-runtime-context-provider.ts#L641-L654)) \n\n   <a href=\"#\"><img alt=\"P2\" src=\"https://greptile-static-assets.s3.amazonaws.com/badges/p2.svg?v=7\" align=\"top\"></a> **`discordWebhookApi` and `googleOAuth2Api` silently unresolvable**\n\n   Both `discordWebhookApi` and `googleOAuth2Api` appear in `MILADY_SUPPORTED_CRED_TYPES` but have no corresponding entry in `CRED_TYPE_FACTS`. The guard `if (!meta) continue` at line 958 silently skips them, so they are never advertised in `supportedCredentials` even when the user has configured them. Either add entries to `CRED_TYPE_FACTS` or remove these types from `MILADY_SUPPORTED_CRED_TYPES` to keep the two sets consistent.\n\n3. `packages/app-core/src/services/n8n-runtime-context-provider.test.ts`, line 351 ([link](https://github.com/elizaos/eliza/blob/4cae1c5dfebabc071b953babb33dbd2477397a3f/packages/app-core/src/services/n8n-runtime-context-provider.test.ts#L351)) \n\n   <a href=\"#\"><img alt=\"P2\" src=\"https://greptile-static-assets.s3.amazonaws.com/badges/p2.svg?v=7\" align=\"top\"></a> **No tests for `triggerContext` propagation**\n\n   The test file exercises Discord facts, Gmail facts, credential filtering, caching, and network failures — but there are no test cases for the new `triggerContext` / `formatTriggerContextFact` path that this PR adds. Adding at least one test per platform (Discord channel, Telegram chat, Slack channel, and the empty-context case) would close the coverage gap and guard against regressions in the fact-line wording.\n\n4. `packages/app-core/src/api/n8n-routes.ts`, line 84-89 ([link](https://github.com/elizaos/eliza/blob/4cae1c5dfebabc071b953babb33dbd2477397a3f/packages/app-core/src/api/n8n-routes.ts#L84-L89)) \n\n   <a href=\"#\"><img alt=\"P2\" src=\"https://greptile-static-assets.s3.amazonaws.com/badges/p2.svg?v=7\" align=\"top\"></a> **Memory ordering not defensively handled**\n\n   The comment notes that `runtime.getMemories` \"typically returns most-recent-first\" but also says the code \"defensively handles either order.\" In practice, `memories.find(m => m.entityId !== runtime.agentId)` returns the **first** matching element — if the API returns oldest-first, this yields the oldest inbound message rather than the most recent one. For an active conversation this could mean routing to a stale channel/chat ID. Consider sorting by a timestamp field (if available) or explicitly documenting the assumed ordering so a future reader knows when this assumption breaks.\n</details>\n\n<!-- /greptile_failed_comments -->\n\n<sub>Reviews (2): Last reviewed commit: [\"fix(n8n): drop Telegram fromId fallback ...\"](https://github.com/elizaos/eliza/commit/975dc2ce12977f594f1dac862d22d44eecf9b4ae) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=30031781)</sub>\n\n<!-- /greptile_comment -->",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-28T15:29:22Z",
      "mergedAt": "2026-04-29T00:07:02Z",
      "additions": 973,
      "deletions": 2
    },
    {
      "id": "PR_kwDOMT5cIs7Wgiim",
      "title": "[codex] Clear stale Eliza Cloud auth on relink",
      "author": "jqmwa",
      "number": 7165,
      "body": "## Summary\n- Clear stale in-memory Eliza Cloud auth when a new account login is persisted.\n- Reinitialize `CloudManager` with the newly linked API key instead of keeping an old client alive.\n- Preserve the linked account identity in runtime secrets/settings.\n- Add regression coverage for `/api/cloud/login/persist`.\n\n## Root Cause\nConnecting a new Eliza Cloud account updated disk config, but the running runtime could continue using old CloudAuth/CloudManager instances. That made account status and inference paths disagree after relink.\n\n## Dependency\nDepends on plugin PR: https://github.com/elizaos-plugins/plugin-elizacloud/pull/16\n\n## Validation\n- Focused Vitest from Milady checkout: 2 files passed, 5 tests passed.\n- `@elizaos/plugin-elizacloud` build passed.\n- Full app-core typecheck/build is currently blocked by unrelated existing LifeOps/native plugin type errors in the checkout.\n",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-28T22:14:39Z",
      "mergedAt": "2026-04-29T02:39:16Z",
      "additions": 908,
      "deletions": 134
    },
    {
      "id": "PR_kwDOMT5cIs7WZBg3",
      "title": "feat(app-core): n8n runtime-context provider — surface Discord guilds/channels + Gmail to workflow generator",
      "author": "2-A-M",
      "number": 7163,
      "body": "## Summary\n\nRegisters an optional service of type `n8n_runtime_context_provider` that the patched `@elizaos/plugin-n8n-workflow` (see #25) reads to inject live connector facts into the workflow-generation prompt:\n\n- **Discord facts** — enumerates the bot's joined guilds + their text channels via the Discord REST API, emits one fact line per guild (`Discord guild \\\"Cozy Devs\\\" (id …) channels: #general (id …), #alerts (id …).`). 5-minute REST cache keeps generate→modify regeneration bursts cheap.\n- **Gmail fact** — surfaces the connected Gmail address so the LLM substitutes the real value verbatim instead of \\`<your-email-here>\\`.\n- **Supported credentials** — only advertises cred types that the host's optional `credProvider.resolve()` confirms have data right now, so we don't promise a credential the user hasn't wired up yet. Without a registered credProvider, falls back to \\\"config has connector token\\\" heuristics.\n\nTogether with the prompt hardening in plugin-n8n-workflow#25, this closes the placeholder-id gap that previously made the LLM emit `guildId: \\\"={{YOUR_SERVER_ID}}\\\"` when the runtime already knew the real ID.\n\n## Why now\n\n#7134's missing-credentials banner can tell the user a credential isn't wired yet. This PR closes the next loop: when credentials *are* wired, the generator gets the real values up front so it doesn't emit placeholders that need post-deploy fix-up.\n\n## Changes\n\n- **New `packages/app-core/src/services/n8n-runtime-context-provider.ts`** (~420 lines, self-contained — `ConnectorConfigLike` and the supported-cred-types set are inlined so this doesn't drag a sibling credential-provider port along).\n- **New tests** (`n8n-runtime-context-provider.test.ts`, 268 lines, 8 unit tests):\n  - returns empty facts when no Discord token is configured;\n  - emits one fact line per guild + caches subsequent calls within TTL;\n  - `supportedCredentials` filtered against `credProvider.resolve()`;\n  - `preferredProviders` derived purely from connector config (no node search);\n  - REST failures degrade to empty facts.\n- **Wire-up in `runtime/eliza.ts`**: `ensureN8nRuntimeContextProvider` follows the same hot-reload pattern as `ensureN8nAuthBridge`/`ensureN8nAutoStart`/`ensureN8nDispatchService`. Picks up the optional `n8n_credential_provider` if one is already registered.\n\n## Backward compat\n\n100% additive. The plugin treats the service as optional — when not registered, the prompt simply omits the new `## Available Credentials` and `## Runtime Facts` sections (current behavior).\n\n## Depends on\n\n- **Runtime depends on elizaos-plugins/plugin-n8n-workflow#25** (RuntimeContextProvider extension point). Host compiles fine without the plugin upgrade, but the prompt facts only take effect once #25 merges and the plugin pointer bumps.\n\n## Test plan\n\n- [ ] `bun run test packages/app-core/src/services/n8n-runtime-context-provider.test.ts` — 8/8 pass.\n- [ ] With a configured Discord bot + Gmail, generate a Discord-routed workflow and inspect the prompt log: `## Runtime Facts` block populated with guild + channel listing.\n- [ ] Without any connector configured: prompt unchanged from `develop` today (sections omitted).\n\n<!-- greptile_comment -->\n\n<h3>Greptile Summary</h3>\n\nThis PR introduces a new `n8n_runtime_context_provider` service that injects live Discord guild/channel IDs and Gmail addresses into the n8n workflow-generation prompt, closing the placeholder-ID gap where the LLM would previously emit `guildId: \\\"={{YOUR_SERVER_ID}}\\\"`. The implementation follows the existing hot-reload pattern in `eliza.ts` and the previously-flagged `CRED_TYPE_FACTS`/`MILADY_SUPPORTED_CRED_TYPES` mismatch has been corrected with a guard comment.\n\n<h3>Confidence Score: 5/5</h3>\n\nSafe to merge; all previous P1 issues have been addressed and remaining findings are P2 style/consistency concerns.\n\nAll P1 issues from previous review rounds are resolved. The two new findings are both P2: a silent guild omission on channel-fetch network errors (vs. the consistent behavior on HTTP errors), and a stale comment in eliza.ts. Neither affects correctness of the happy path.\n\npackages/app-core/src/services/n8n-runtime-context-provider.ts — the per-guild catch block (lines 291–300) should push a fallback fact line for consistency.\n\n<h3>Important Files Changed</h3>\n\n| Filename | Overview |\n|----------|----------|\n| packages/app-core/src/services/n8n-runtime-context-provider.ts | New service (~420 lines) that surfaces Discord guild/channel IDs and Gmail address to the n8n workflow generator; previous P1 issues (CRED_TYPE_FACTS mismatch, cache key) are addressed with code comments; one new P2: thrown per-guild channel fetches silently drop the guild from facts instead of pushing a fallback line. |\n| packages/app-core/src/runtime/eliza.ts | Wires up `ensureN8nRuntimeContextProvider` following the established hot-reload pattern; minor: inline comment about \"config has connector token\" fallback does not match the actual implementation. |\n| packages/app-core/src/services/n8n-runtime-context-provider.test.ts | 268-line test suite covering 8 scenarios including guild enumeration, caching, credProvider filtering, and graceful network failure degradation; coverage is adequate for the new service. |\n\n</details>\n\n<h3>Sequence Diagram</h3>\n\n```mermaid\nsequenceDiagram\n    participant Plugin as plugin-n8n-workflow\n    participant Provider as N8nRuntimeContextProvider\n    participant CredProv as n8n_credential_provider\n    participant Discord as Discord REST API\n    participant Cache as discordCache (in-process)\n\n    Plugin->>Provider: getRuntimeContext({userId, relevantNodes, relevantCredTypes})\n    Provider->>CredProv: resolve(userId, credType) [for each relevantCredType]\n    CredProv-->>Provider: {status: credential_data} | {status: needs_auth}\n    Note over Provider: Filter supportedCredentials to resolved types only\n\n    alt Discord node in relevantNodes and token configured\n        Provider->>Cache: get(botToken)\n        alt Cache hit within 5 min TTL\n            Cache-->>Provider: cached facts[]\n        else Cache miss\n            Provider->>Discord: GET /users/@me/guilds\n            Discord-->>Provider: [{id, name}, ...]\n            loop per guild\n                Provider->>Discord: GET /guilds/{id}/channels\n                Discord-->>Provider: [{id, name, type}, ...]\n            end\n            Provider->>Cache: set(botToken, {facts, expiresAt})\n        end\n    end\n\n    alt Gmail node in relevantNodes and email configured\n        Note over Provider: Push Connected Gmail account email\n    end\n\n    Provider-->>Plugin: {supportedCredentials[], facts[]}\n```\n\n<sub>Reviews (2): Last reviewed commit: [\"fix(n8n-runtime-context): drop discordWe...\"](https://github.com/elizaos/eliza/commit/9975edb8ba6a363234c01593774104e9dd6944e6) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=30030820)</sub>\n\n<!-- /greptile_comment -->",
      "repository": "elizaos/eliza",
      "createdAt": "2026-04-28T15:23:14Z",
      "mergedAt": "2026-04-29T00:06:55Z",
      "additions": 751,
      "deletions": 0
    }
  ],
  "codeChanges": {
    "additions": 0,
    "deletions": 0,
    "files": 0,
    "commitCount": 87
  },
  "completedItems": [],
  "topContributors": [
    {
      "username": "2-A-M",
      "avatarUrl": "https://avatars.githubusercontent.com/u/96268540?u=b7d92c0e2a91af580d09eeae862eef576955ab8a&v=4",
      "totalScore": 215.00061770784046,
      "prScore": 214.80061770784047,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0.2,
      "summary": null
    },
    {
      "username": "lalalune",
      "avatarUrl": "https://avatars.githubusercontent.com/u/18633264?u=e2e906c3712c2506ebfa98df01c2cfdc50050b30&v=4",
      "totalScore": 180.01466737779978,
      "prScore": 179.8146673777998,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0.2,
      "summary": null
    },
    {
      "username": "RemilioNubilio",
      "avatarUrl": "https://avatars.githubusercontent.com/u/275382225?u=b1501ee01bb54e5b31ca64895f2a07c69f554a37&v=4",
      "totalScore": 88.9442181402252,
      "prScore": 88.9442181402252,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "Dexploarer",
      "avatarUrl": "https://avatars.githubusercontent.com/u/211557447?u=21a243d61cc1f87574328ae07fc64d7d7577b53d&v=4",
      "totalScore": 87.0875477931522,
      "prScore": 87.0875477931522,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "jqmwa",
      "avatarUrl": "https://avatars.githubusercontent.com/u/95416268?u=aa05f4d49acc8c161a83f6a313231f63904521e9&v=4",
      "totalScore": 83.23792449022048,
      "prScore": 83.23792449022048,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "standujar",
      "avatarUrl": "https://avatars.githubusercontent.com/u/16385918?u=718bdcd1585be8447bdfffb8c11ce249baa7532d&v=4",
      "totalScore": 59.5437738965761,
      "prScore": 59.5437738965761,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "greptile-apps",
      "avatarUrl": "https://avatars.githubusercontent.com/in/867647?v=4",
      "totalScore": 49.5,
      "prScore": 0,
      "issueScore": 0,
      "reviewScore": 49.5,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "odilitime",
      "avatarUrl": "https://avatars.githubusercontent.com/u/16395496?u=c9bac48e632aae594a0d85aaf9e9c9c69b674d8b&v=4",
      "totalScore": 44.3817738965761,
      "prScore": 39.3817738965761,
      "issueScore": 0,
      "reviewScore": 5,
      "commentScore": 0,
      "summary": null
    },
    {
      "username": "sailorpepe",
      "avatarUrl": "https://avatars.githubusercontent.com/u/159828807?u=9298c48eb8b41b0a0b0968e987c029e307771855&v=4",
      "totalScore": 0.2,
      "prScore": 0,
      "issueScore": 0,
      "reviewScore": 0,
      "commentScore": 0.2,
      "summary": null
    }
  ],
  "newPRs": 24,
  "mergedPRs": 0,
  "newIssues": 0,
  "closedIssues": 0,
  "activeContributors": 8
}