fix: restore interactive cloud login tests + evidence gate - #2
Open
sharkwon wants to merge 581 commits into
Open
fix: restore interactive cloud login tests + evidence gate#2sharkwon wants to merge 581 commits into
sharkwon wants to merge 581 commits into
Conversation
(cherry picked from commit 950228da0866e9a820273da1ac45234660468506)
(cherry picked from commit 0dcaf1409bfcf1a9b5acf49d4847bbb75af7c765)
Extends elizaOS#17715 with the typed J4 source-state boundary found during live preview QA. AI provider/model: OpenAI / gpt-5.6-sol Client / agent tooling: Codex desktop Contribution skill revision: 04ff898:packages/skills/skills/contribute-to-eliza Attribution status: self-reported — [codex/restack-demo-0804] eliza-computer-attribution:v1 {"agent":"codex","provider":"openai","model":"gpt-5.6-sol","client":"codex-desktop","skill_revision":"04ff8982861e7aa5045c0a065fb7e1d8750e6e6b:packages/skills/skills/contribute-to-eliza","status":"self-reported","marker":"codex/restack-demo-0804"}
…box character eager init (elizaOS#17899) * feat(agent): configurable deferred plugin registration timeout + sandbox character eager init Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(agent): clear the deferred-registration timer and report the boot-hook failure Three defects in the deferred plugin registration path: - The timeout promise never cleared its `setTimeout`, so every deferred registration left a live 30-60s timer holding the event loop open past process exit. Clear it in `finally` and unref it. - `let sandboxRouteAgentId` is never reassigned; biome failed the lint gate on it, which is what turned this PR red. - The local-inference boot hook swallowed its failure into a bare `logger.warn`, with no `error-policy:J<N>` marker. Classify it as J4 (voice/ASR readiness reports the degraded state through its own route) and route it through `runtime.reportError` so it reaches RECENT_ERRORS. Documents that the deadline bounds only how long boot waits — registerPlugin has no cancellation channel, so registration may still land afterwards. `bun run lint:check` now passes clean across all 843 files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: elizaOS Developer <developer@elizalabs.ai> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: lalalune <autonomousresearcher@gmail.com> Co-authored-by: Shaw <shawmakesmagic@gmail.com>
…icated field to status (elizaOS#17894) * fix(agent): add GET /api/auth/pair-code loopback endpoint and authenticated field to status Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(agent): correct the pairing security note and cover the token-as-code path The token-as-pairing-code branch shipped with no tests and a comment claiming "the timing-safe comparison prevents brute-force". It does not — `timingSafeEqual` removes the timing side-channel; the per-IP `rateLimitPairing` above is what bounds guessing. Rewrite the note to state what actually protects the path, and to record that it deliberately bypasses the pairing code's expiry window, so the token's own entropy becomes the bound. Adds coverage pinning those gates: exchange succeeds, whitespace is tolerated, a near-miss is rejected, and the path is still refused when rate limited or when pairing is disabled. Also fixes the biome formatting that turned this PR red; `bun run lint:check` passes clean across 843 files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: elizaOS Developer <developer@elizalabs.ai> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: lalalune <autonomousresearcher@gmail.com> Co-authored-by: Shaw <shawmakesmagic@gmail.com>
…account-vault-key fix(ci): make multi-account E2E keychain-independent
…lizaOS#17613) * fix(eliza-computer): scope footer validation to the terminal attribution block The PR template ships `Client / agent tooling`, `Skill revision`, and `Attribution status` as checklist rows, and SKILL.md instructs appending the attribution footer after the template. markerFooterError counted those labels across the WHOLE body and required exactly one of each, so following both documents always produced an invalid marker. The published ledger shows the damage: 3 valid against 64 missing-or-invalid of 67 eligible sources, including PRs authored by the project lead. Attribution states add no points (leaderboard.ts:1379), so nothing about scoring changes; what was broken is the provenance dataset the whole attribution system exists to produce. Footer rows are now counted only within the terminal attribution block — the contiguous run of label lines ending at the validated lane signature — so a checklist row elsewhere in the body is not mistaken for a second footer. Duplicate detection is unweakened: rows abutting the lane are still part of that block and still collide, and the at-most-one-marker gate is untouched. Tests: the real template's three rows above an appended footer now validate; adjacent duplicated rows still fail with the original reason; all 52 existing cases unchanged. Closes elizaOS#17610 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgpHguHoGxK4pVEtZ6gY43 * fix(evidence): insert before a trailing attribution footer patchRow and patchEvidenceHead appended new markers/rows at the end of the PR body when their target marker was absent. When the body ends with the eliza-computer-attribution:v1 footer that SKILL.md instructs every contributor to append last, that append relocated content after the terminal marker, and the validator (leaderboard.ts:1063) rejects it: 'marker must be the final source content'. Add findTrailingAttributionFooterStart, which locates a trailing attribution footer using the same shape the validator enforces (the v1 marker as final content, exactly one terminal lane signature line above it, and the contiguous run of attribution label lines above that, optionally preceded by a '---' separator). Both patch functions now insert before that footer instead of after it when it is present, and fall back to the existing append behavior otherwise. Fixes elizaOS#17610 acceptance criterion 4. * style(evidence): satisfy biome formatting on every file this PR touches `biome check` (root biome.json, lineWidth 80) was red on three lines this branch introduced. The leaderboard.test.ts one predates today's work — it came in with the terminal-block validator commit and would fail the eliza-computer package's own `biome check .`: packages/eliza-computer/src/lib/leaderboard.test.ts:376 reason: "marker requires exactly one complete visible attribution footer", scripts/pr-evidence.mjs:352 records.push({ ... trimmed: ... }); scripts/pr-evidence.test.mjs:36 readFileSync(fileURLToPath(new URL(...))) Formatting only — no behaviour change. Applied with biome itself rather than by hand, so the result is exactly what the formatter would print. Verified after the change: biome check (4 changed files) clean node --test scripts/pr-evidence.test.mjs 35 pass / 0 fail bun test packages/.../leaderboard.test.ts 54 pass / 0 fail Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(eliza-computer): validate the SHIPPED template file, not an excerpt Issue elizaOS#17610's first acceptance criterion asks for an executable test against the real template. The existing elizaOS#17610 test reconstructs the template's attribution rows; this one reads .github/pull_request_template.md from disk and appends the SKILL.md footer, so the test breaks if either document drifts back into incompatibility. Fails against the pre-branch validator (footer-shaped label rows counted body-wide), passes with terminal-block scoping. * docs(eliza-computer): correct which rule guards lane-separated footers Fixes the overstated comment above the terminalAttributionBlock call in markerFooterError. Adjacent duplicate rows collide inside this terminal block via the row-count checks below; two COMPLETE footers each carrying their own lane signature are rejected earlier by the one-terminal-lane- signature rule, not by this block's counts. Adds a test pinning that lane-separated shape: two complete footers with different lane signatures followed by one v1 marker is rejected with 'marker requires exactly one terminal lane signature' and yields no machine-marker declaration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(eliza-computer): resolve template fixture without import.meta.url in jsdom import.meta.url is not a file: URL for modules under src/** when vitest runs with environment: jsdom, so fileURLToPath(new URL(...)) threw 'The URL must be of scheme file' for the SHIPPED-template test added in fbc84b5. Resolve the repo-root fixture from process.cwd() instead, matching how the test is actually invoked (bun run --cwd packages/eliza-computer test, both locally and in CI). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… boundary (elizaOS#17604) * fix(core): give coalesced composeState waiters their own cancellation boundary providerExecutionsInFlight stored one promise wired to the FIRST caller's signal and deadline; a later coalesced caller awaited it verbatim. The turn registry maps a room to its newest controller, so the realistic sequence — turn A still streaming, turn B re-delivers the same message and coalesces, user taps stop (aborting B) — swallowed the stop entirely: B neither rejected nor resolved until A's provider settled, then completed as if never cancelled. Symmetrically, A's abort rejected B with A's TurnAbortedError. The execution now owns a dedicated AbortController and every caller (owner included) awaits through awaitProviderExecution: a per-waiter race against the caller's OWN signal with reference counting, so an aborting waiter rejects immediately with its own reason, the shared work keeps running for remaining callers, and the provider is aborted exactly when the last interested caller is gone — a lone caller's abort still reaches the provider as before (the existing cancellation test's semantics are unchanged). Deterministic regression test drives two overlapping turns on one room: red on the previous code ('swallowed'), green now, with the owner completing untouched. Closes elizaOS#17602 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgpHguHoGxK4pVEtZ6gY43 * fix(core): count signal-less callers in the provider-execution refcount Review finding on elizaOS#17604 (verified by executing awaitProviderExecution standalone): a caller with no signal took the early return and never joined the waiter count, but the count is what authorizes aborting the shared work — so a cancelling waiter could kill a provider an uncounted caller was still awaiting. providerSignal is legitimately undefined for composeState outside any turn or streaming context, making the sequence reachable: X composes signal-less and owns the execution, Y coalesces under a turn, Y is stopped, waiters hits zero, the shared provider is aborted, and X's composeState fails with PROVIDER_COMPOSITION_FAILED for a stop it never issued. Every attached caller now increments the count; signal-less callers release on settle via finally. Regression test drives the reviewer's exact arrangement and is red on the previous head (providerAborted true, X's composition failing) and green now, with X completing untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgpHguHoGxK4pVEtZ6gY43 * style(core): biome format for the new coalescing tests * fix(core): evict in-flight provider execution before abort, not after settle krutftw's elizaOS#17604 review traced a real race: awaitProviderExecution's sole-waiter abort path calls execution.controller.abort() synchronously, but the composeState call site only removes the in-flight map entry once the shared promise finishes unwinding through withProviderDeadline and withProviderStep — a microtask or more later. A composeState call for the same message arriving in that window still gets() the dying execution and inherits the departed caller's abort reason instead of running its own provider execution. Add a red test that aborts the sole waiter of a coalesced execution and, without yielding long enough for cleanup to run, issues a fresh composeState for the same message with a fresh signal — it reproduces the race against unmodified runtime.ts. Fix: give awaitProviderExecution an `evict` thunk invoked synchronously immediately before controller.abort(), so the map entry becomes unreachable the instant it stops being viable. Hoist a single identity-checked, idempotent eviction thunk at the composeState call site so both the owner path (which creates the execution) and the coalesced path (which reuses one fetched from the map) can call it; it also still fires from promise.then(evict, evict) as a backstop. Also documents the waiters-field invariant: every consumer must attach via awaitProviderExecution, not by awaiting execution.promise directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(core): attach settle handlers before checking signal.aborted The already-aborted-signal early return in awaitProviderExecution called onAbort() (which aborts the shared controller) without ever attaching a rejection handler to execution.promise when inFlightKey is null (e.g. a refreshSet provider recompose). The shared promise's eventual rejection then had no listener, producing an unhandled promise rejection under Node's default unhandled-rejection behavior. Move the execution.promise.then(settle(resolve), settle(reject)) call ahead of the signal.aborted check so a handler is always attached before any abort can fire, regardless of whether the caller's signal was already aborted on entry. Addresses elizaOS#17604 review follow-up. * fix(core): abort in-flight provider work on runtime stop Addresses @lalalune's two non-blocking review items on elizaOS#17604. 1. Comment provenance removed. Roughly 53 added comment lines in runtime.ts carried review history ("caught in review on elizaOS#17604 by executing this function standalone", "the same defect this file fixes, relocated from 'always' to 'inside a race window' (elizaOS#17604 review)"). CLAUDE.md asks for change history and status notes to be deleted, so each now states the invariant in the present tense without the provenance. Two test titles carrying "elizaOS#17604 review" were renamed the same way; "(elizaOS#17602)" stays, since that is the issue being closed rather than a note about how the defect was found. 2. Runtime stop aborts the shared provider work. `stop()` cleared `providerExecutionsInFlight` without firing each execution's AbortController, and that controller is reachable only through the map, so dropping the entries stranded in-flight provider calls past teardown with nothing left able to cancel them. Pre-existing, but this PR introduces the controller that makes it a three-line fix. Verification: new test "aborts in-flight provider work when the runtime stops" drives a hanging provider through composeState, calls stop(), and asserts the provider's own signal fired. Mutation-checked — removing the abort loop fails it with "expected false to be true", so it is not vacuous. 11/11 in the file, 37/37 across the 8 compose-state suites, Biome clean. Note for anyone reproducing in a fresh worktree: packages/core tests need `node ../shared/scripts/generate-keywords.mjs` first, or collection dies on the gitignored i18n codegen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011gZy3feiGVKs61PDUzx1Cd --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…aOS#17742) * fix(ui): route gateway LAN discovery through the desktop bridge Gateway discovery never returned an endpoint on desktop, so the app could not find a locally-running agent over the LAN even though the backend was live. Two defects on the existing path, both in the UI bridge: 1. `getPluginCapabilities().gateway.discovery` gated on `isNative` alone. Electrobun desktop reports `isNative === false`, so `isFeatureAvailable ("gatewayDiscovery")` was false there and `discoverGatewayEndpoints` returned early before scanning — despite the bonjour-service mDNS backend being wired and reachable behind the `gateway:*` desktop RPC methods. 2. `discoverGatewayEndpoints` only ever read the Capacitor plugin registry. Desktop has no Capacitor registry, so `getPlugins().gateway.plugin` is `{}` and the scan short-circuited on the missing `startDiscovery`. Desktop `startDiscovery` also differs from the Capacitor implementations: it resolves as soon as the bonjour browser is armed, carrying only its warm cache (empty on a cold scan) while results land later on `up` events. Returning that payload would always report zero gateways, so the desktop lane now waits out the scan window and reads the settled set back via `gateway:getDiscoveredGateways`, falling back to the start payload's warm cache. Teardown stays best-effort in `finally`, and the UI owns the stop so the collect read happens in-session. No protocol, transport, or contract changes — the mDNS backend, RPC schema, and endpoint shape are all untouched and were already correct. Tests cover both transports: native scan/teardown/malformed-endpoint filtering, desktop deferred collection, warm-cache fallback, absent bridge, throwing bridge, and the capability gate. Verified load-bearing: reverting the transport routing fails 5, reverting the deferred collection alone fails 1. * fix(ui): report desktop discovery teardown failures * fix(ui): harden desktop gateway discovery outcomes * test(ui): preserve logger contract in gateway discovery --------- Co-authored-by: 0xSolace <shadow@shad0w.xyz> Co-authored-by: Shaw <shawgotbags@gmail.com>
…abel (elizaOS#17943) rowValue() drops an optional human label ("Skill revision: <value>") by stripping to the first colon. The strip was unbounded, so it could not tell a label's colon from one inside the value — and two required rows carry a colon by construction. skill-revision's documented format is owner/repo@<40-hex>:path. Written bare, as the failure message prescribes it, the strip reduced it to just the path and the row was then rejected by that same message: <!-- attribution-row:skill-revision --> `elizaOS/eliza@0123...4567:packages/skills/skills/contribute-to-eliza` -> parsed as `packages/skills/skills/contribute-to-eliza` -> "Skill revision must be owner/repo@full-commit-sha:path or ..." The identical value behind a label passes, because the label supplies the first colon and absorbs the strip — so the gate accepted the template's spelling and rejected the documented one. A routed model identifier such as `bedrock/anthropic.claude-3:1` failed the same way one row over, truncated to `1` and then reported as a placeholder rather than as a model. A label is prose and never carries the `@` or `/` that make a prefix part of an identifier, so an identifier-shaped prefix now keeps its colon. The strip is otherwise unchanged: `Client / agent tooling:`, whose label does contain a slash, still reduces to its value, as does every other row in the repository's own template. Not fixed here, and reported separately: a free-prose ai-assistance value containing a colon ("yes - used for: diagnosis") is still truncated, and then reported as not beginning with yes or no when it does. That row's documented format has no colon, and the fix for it is a different change with its own regression surface.
…aving) (elizaOS#17922) * fix(scenario-runner): keep multi-turn active-view planner fixtures matching The deterministic active-view agent-surface scenario required the full Active View element block in every ACTION_PLANNER prompt. After the first interact that block can be omitted or relocated, so the second-turn click fixture failed closed, the turn synthesized Stage-1 progressive replyText (\"Saving the active ledger.\"), and develop cert went red on a single assertTurn (elizaOS#17918). Match the click/fill planner fixtures on turn input + VIEWS surface only; the interact tool-call remains hardcoded. Local deterministic run passes. Fixes elizaOS#17918 * fix(agent): keep Active View element surface on multi-turn planners lalalune CR on elizaOS#17922 correctly rejected loosening the scenario matcher: the cert exists to assert the addressable-element surface reaches the ACTION_PLANNER. Three product gaps stacked to make turn-2 lose that surface: 1. setActiveViewContext replaced the whole context on re-navigate, dropping elements when the navigate route republished the same viewId without them. 2. applyActiveViewAwarenessToMessages annotated the first user message, not the last, so multi-turn history never put the block on the live turn. 3. Active View was injected before budget compaction, so conversation compaction could leave a header without the element list. Fix all three, restore promptHasActiveViewElements on both planner fixtures, and keep the scenario green with the full surface assertion. Fixes elizaOS#17918 * fix(agent): satisfy biome on Active View multi-turn changes
* fix(memory): harden canonical cross-connector recall * docs(telegram): describe poller lock behavior * fix(memory): close canonical recall review gaps --------- Co-authored-by: Shaw <shawgotbags@gmail.com>
* fix(homepage): add payout profile editor # Conflicts: # packages/homepage/package.json # packages/homepage/src/App.tsx # packages/homepage/tests/e2e/live-routes.spec.ts # packages/homepage/tests/e2e/route-coverage.spec.ts # packages/homepage/tests/e2e/visual.spec.ts * fix(homepage): harden payout profile redirects and validation * test(homepage): baseline payout profile visuals * fix(homepage): enforce EIP-55 wallet checksums * test(homepage): align payout snapshots with inventory --------- Co-authored-by: Shaw <shawgotbags@gmail.com>
…es instead of the canned sentence (elizaOS#17951) a planner turn ending on a failed step shipped the fixed "i tried to complete that, but the available runtime step failed before it produced a usable result" sentence from planner-loop.ts, discarding the failure cause the models already had in context. structural fix, context-in model-out: - a FINISH that declares success:false is a structural failure acknowledgment, so the evaluator's own user-safe diagnosis now ships through the failure authority instead of being replaced by the generic sentence - the silent-failed-finish retry instruction now names the failed tool's human-readable cause (scrubbed of absolute paths/uuids/hex ids) so the replan can pick a real alternative or state the blocker - new honest-failure post-pass: a finished turn whose reply would be the canned sentence gets one forced no-tools synthesis pass primed with the failed step and its scrubbed cause; the model writes the failure reply in its own voice. the fixed sentence remains only as the J4 degrade when that synthesis model call itself fails - evaluator prompt gains a rule to state what failed and why on failed finishes, in everyday language without paths/ids/logs fixes elizaOS#17948 Co-authored-by: NubsCarson <nubscarson@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): fail fast on unapproved fork workflows * fix(ci): complete fork approval recovery contract * fix(ci): reject malformed workflow-run pages
… shutdown (elizaOS#17749) * fix(discord): drain in-flight turns and reconcile status reactions on shutdown `DiscordService.stop()` destroyed debouncers, managers, and clients immediately, with nothing tracking in-flight `handleMessage` calls — so a shutdown mid-turn tore the client out from under the turn, and the status reaction that turn had applied was left showing "in progress" forever. Each reaction controller was a bare closure inside `handleMessage`, so shutdown had no handle to reconcile it with even if it had waited. A turn registry now tracks each in-flight turn and the status-reaction controller it drives. `stop()` awaits that drain BEFORE any teardown, bounded by DISCORD_SHUTDOWN_DRAIN_TIMEOUT_MS (10s): a hang blocks process shutdown indefinitely, which is worse than abandoning loudly. When the bound elapses, every still-running turn's reaction is forced to its terminal marker and the abandonment is logged with structured context (observed turns, abandoned message ids, the timeout) — never silent. `StatusReactionController` gains `whenFinished` and `abandon()`, reusing its existing terminal transition; `handleMessage` becomes a thin wrapper that registers with the registry and delegates to the unchanged turn body. Deliberately NOT claimed by this change, and still outstanding on elizaOS#16318: no inbound-ingress cordon (discord.js delivers gateway events until the client is destroyed; a cordon belongs to the runtime shutdown path), no supervisor-chain change (dev-ui SIGKILLs at 1.5s, shorter than this drain), and no startup reconciliation of reactions stranded by a prior hard kill. Tests: 10 new across two files — drain completes an in-flight turn without abandoning its reaction, zero in-flight returns promptly, and the timeout abandons + logs rather than hanging, at both registry and `DiscordService#stop` level. Verified red behaviourally: with the new module present and only `stop()`'s drain reverted, both service-level cases fail. Package suite 72 files / 567 tests green (baseline 70/557). Refs elizaOS#16318 * fix(discord): clear the drain timer so a clean shutdown does not hold the loop Review catch (@krutftw, elizaOS#17749): the timeout handle in `drain` was never captured, so there was no `clearTimeout`. When `settleAll` won the race — the normal case, every in-flight turn finishing promptly — `drain` returned immediately but the timer stayed armed for the full DISCORD_SHUTDOWN_DRAIN_TIMEOUT_MS. An active Node timer keeps the event loop alive, so a shutdown that reads as instant in the logs still delayed process exit by up to 10 seconds, and every Discord log line said success — precisely the misdiagnosis this module exists to prevent. Capture the handle and clear it in a `finally`, so the timer is released on both branches of the race. `clearTimeout` rather than `unref()`: it also releases the closure promptly, and the zero-turn early return already avoids arming a timer at all. Regression test spies on setTimeout/clearTimeout to assert nothing stays armed once a fast drain settles; it fails against the previous implementation (armed.size === 1, clearTimeout never called) and passes here. Package suite 72 files / 568 tests green. * fix(discord): await the status reaction in the drain, not just the handler Second review catch (@krutftw, elizaOS#17749): `settleAll` awaited only `entry.promise`. `resolveFinished()` fires inside the status-reaction controller's serialised `chain`, so it lands strictly AFTER the handler promise — a fast turn could settle the drain while its reaction was still mid-transition, `timedOut` would stay false, no `abandon()` would run, and `stop()` would destroy the client on top of a reaction still showing in progress. That is the exact state this module exists to prevent, reached through the success path instead of the timeout branch. It also made the success path's `observedInFlightTurns` mean less than the log line implies to a reader. The drain now awaits `[handler, statusReactions.whenFinished]` per entry, so a clean drain means "handler done AND reaction reconciled". The `turns.has(messageId)` check still governs abandonment, so the timeout branch is unchanged. Regression test holds a controller mid-transition after its handler has already resolved and asserts the drain has not settled; it fails against the previous implementation and passes here. Package suite 72 files / 569 tests green. * fix(discord): retire a turn only when handler AND reaction are both done Three review catches from @wtfsayo, all downstream of the previous fix that started awaiting `whenFinished`: 1. The success-path service test passed via the TIMEOUT, not the drain. Its fake controller never settled, so `settleAll` could not win and the case burned the full 10s while claiming to prove the fast path — a false green in the exact class this PR is about. The fixture now reconciles its reaction the way a real turn does (`setDone` on the way out), and an elapsed-time assertion pins it: 10002ms -> 7ms. 2. A turn was untracked as soon as its HANDLER settled, so one whose reaction was still mid-chain had already left the registry when the drain snapshot was taken. It was neither awaited nor abandoned: the reaction stayed stranded in-progress AND the drain reported the success path. A turn now leaves the registry only when both halves are done, and abandonment reads the entry's own reaction state rather than map membership. 3. `abandon()` only enqueues the terminal transition on the controller's serial chain, so the drain returned before the Discord call landed and `stop()` destroyed the client mid-reconcile. Abandoned controllers are now awaited under their own tighter ceiling (DISCORD_REACTION_RECONCILE_TIMEOUT_MS, 2s, unref'd) — this path is already the one where something is not finishing, so the second wait is deliberately short rather than generous. Registry fixtures that modelled a handler finishing while its reaction never settled were encoding the old gating; they now model real turns, and the stranded case @wtfsayo asked for is covered explicitly (it fails against the previous implementation, which reported no abandoned ids). Package suite 72 files / 570 tests green; the three service cases run in 3ms / 7ms / 2ms. * fix(discord): report drain timeouts and stop leaking turn registry entries Addresses @lalalune's review on elizaOS#17749. 1. The timeout path reported success. stop() branched on abandonedMessageIds.length, but status reactions are scope-gated — scope "none" and un-addressed guild messages under "group-mentions" produce no controller — so on a typical server most turns can hang through the entire bound while contributing nothing to that array. Those shutdowns logged "Drained N in-flight turn(s) before shutdown" while dropping the work. DiscordDrainResult now carries timedOut and unfinishedMessageIds (work dropped) as distinct from abandonedMessageIds (reactions forced terminal), and stop() branches on timedOut. 2. Registry entries could leak permanently. The single-entry design retired a turn only once BOTH halves finished, so any throw escaping the inner handlers with a live controller left whenFinished pending forever, pinned the entry, and made every later stop() burn the full 10s bound. Fixed at both ends: messages.ts now drives the controller terminal in its outer catch (the declaration is hoisted out of the try, where it was not even in scope), which also fixes a user-visible bug — a failed turn left its message showing the "thinking" emoji forever. And the registry now tracks the two halves in separate maps that retire independently, so the same mistake at a future call site costs one bounded drain instead of permanent growth. Draining the union of both maps keeps @wtfsayo's property: a turn whose handler settled while its reaction is mid-chain is still visible to drain. 3. trackTurn aliasing is now guarded. Retirement compares promise identity instead of mutating a shared entry object, so an older promise settling cannot retire a re-registered turn. Not theoretical: the new test fails against the previous implementation with pendingCount 0 instead of 1. Verification: 14/14 in __tests__/shutdown-drain.test.ts, and 6 of them fail against the previous implementation (mutation-checked by reverting shutdown-drain.ts to HEAD and re-running). Full plugin suite 574/574 across 72 files. Biome clean. Package typecheck still reports the 5 pre-existing errors that reproduce at merge-base (@elizaos/plugin-commands unbuilt); none are in the touched files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011gZy3feiGVKs61PDUzx1Cd --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…lizaOS#17838) * test(agent): discover packages/agent integration suites by pattern packages/agent's own lanes exclude the `.integration.test.` suffix (vitest.config.ts and scripts/run-vitest-batches.mjs), so the repo-level integration config is the only lane that can run those files. Its include list covered packages/agent/test/** but not packages/agent/src/**, and no CI job invoked it for the agent package at all — both existing callers pass an explicit plugin-personal-assistant file list, which narrows the run regardless of the include globs. A dot-named suite anywhere under packages/agent therefore ran in no lane. Adds the missing src/** include glob, a pattern-scoped test:integration script for the package (no file list, so new suites need no wiring), a CI step that runs it, and a regression test asserting both agent roots stay covered. Fixes elizaOS#17778 * docs(agent): list test:integration in the package guide pair * fix(agent): make agent integration lane non-vacuous Address lalalune CR on elizaOS#17838: - Rename views-registry-integration.test.ts -> views-registry.integration.test.ts so the packages/agent/src/**/*.integration.test.ts glob discovers a real suite - Drop --passWithNoTests from test:integration so empty collection fails - Remove hand-enumeration of the suite from test.yml Views system tests and packages/agent test:remote-capabilities - Fold glob-coverage assertions into the existing agent subpath-alias test and delete the near-duplicate globs file Verified: bun run --cwd packages/agent test:integration -> 37 passed
…lizaOS#17739) * revert: homepage waitlist as canonical deploy source (elizaOS#17736) Shadow called the waitlist messaging wrong; production was rolled back to the pre-waitlist deployment (088b3bd5) via Pages rollback. This revert restores deploy-homepage.yml to building packages/homepage so the deploy pipeline matches the restored production surface, and removes the waitlist source directory. If a waitlist returns, it will be re-scoped with corrected messaging first. Co-authored-by: Sol <sol@shad0w.xyz> * fix(homepage): restore fail-closed deployment gates --------- Co-authored-by: 0xSolace <shadow@shad0w.xyz> Co-authored-by: Shaw <shawgotbags@gmail.com>
…OS#17686) * fix(scripts): replace rg with git ls-files in packageJsonPaths Removes undeclared ripgrep dependency. Uses git ls-files which is a hard requirement everywhere. Adds ENOENT error handling to report missing git binary instead of generic failure message. Closes elizaOS#17681 * fix(scripts): keep packageJsonPaths matching only real package.json files git's `*package.json` pathspec is a suffix match, so it would also return a stray `foo-package.json` that `rg --files -g package.json` never matched. It happens to return the same 153 files today, which is exactly the kind of equivalence that stops being true silently. Filter on the basename so the replacement is behaviour-preserving by construction rather than by coincidence. --------- Co-authored-by: lalalune <shawmakesmagic@gmail.com>
* fix: repair Windows verification contracts # Conflicts: # packages/app-core/scripts/ensure-shared-i18n-data.mjs # packages/import-conversations/package.json * docs: sync native gateway lint guidance --------- Co-authored-by: Shaw <shawmakesmagic@gmail.com>
…efore React boots (elizaOS#17893) * fix(agent): seed elizaos:active-server accessToken in injected HTML before React boots Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(agent): keep window.__ELIZA_API_TOKEN__ alongside the boot-config seed Seeding boot-config apiToken is the right fix — getElizaApiToken() reads only the boot-config store, so the bare window global never reached it. But the global is not dead: plugin-native-agent and plugin-native-websiteblocker read `window.__ELIZA_API_TOKEN__` directly (documented in both plugins' README/CLAUDE/AGENTS) and the Android WebView hydrates it. They fall back only to sessionStorage, which nothing seeds, so dropping it sent their API calls unauthenticated. Emit all three sinks and document why each exists, including the first-run-complete write, which only applies to already-provisioned deployments. Replaces the substring assertions with tests that execute the injected script against a stubbed browser global, so a syntactically broken seed can no longer pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: elizaOS Developer <developer@elizalabs.ai> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: lalalune <autonomousresearcher@gmail.com> Co-authored-by: Shaw <shawmakesmagic@gmail.com>
…ugin (elizaOS#17871) Co-authored-by: Jeremy <jeremy@webtop-dev.0mod.com> Co-authored-by: Shaw <shawmakesmagic@gmail.com>
* fix(ci): harden snap-publish workflow inputs
Move workflow_dispatch version/channel into env: and expand via shell
variables instead of interpolating ${{ }} into run: script text.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: re-trigger PR gates after snap-publish hygiene
Empty commit to re-run Develop/Security Advisory gates that timed out.
* ci(snap-publish): validate version/channel before sed and upload
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
… and workflows (elizaOS#17599) * fix(ci): re-land the review-hardened Bun contract on the refactored tree develop adopted this PR's v1 skeleton (ci-bun-version.json + the early line-based contract) via the refactor sweeps, then the same sweeps regressed most of the site pins the contract exists to hold: bare bun.sh/install in the deploy workflows and training cloud scripts, presence-only install guards, a 1.3.10 default in docker-ci-smoke, a 1.3.13 Hetzner cloud-init download, a canary cell and mutable @v2 in the computeruse CI template, and the contract execution dropped from BOTH required lanes. Live demonstration of the drift class elizaOS#17044 names. This commit is the review-hardened v2 contract (job-scoped structured YAML resolution per maintainer review, Docker stage scoping, shell default proving, job-scoped allowlist, 55-test suite) re-anchored to the refactored tree: the gate list drops the two lanes the refactor deleted (benchmark-tests, feed-env-audit), every regressed site is re-pinned, and both required lanes execute the contract with the exact-head inventory artifact again. Contract green: 333 sites, 9 gate lanes, zero drift classifications. * fix(ci): scan the surfaces the Bun contract was selecting itself out of Addresses @lalalune's four findings on elizaOS#17599, rebased onto develop. 1. The scan precondition excluded exactly what invariant 2 targets. A non-.github YAML entered the inventory only if it already contained a `bun-version:` key — but a `oven-sh/setup-bun` step that wires no version has no such key, so the defective files selected themselves out and the gate reported a clean sweep of a set chosen to exclude its own counterexamples. 16 plugin workflows were running the action's floating "latest". The scan now also admits any file referencing oven-sh/setup-bun, which surfaced 30 violations on the first run, and all 15 files are pinned to the reviewed SHA plus bun-version 1.3.14. 2. test.yml had two contract invocations. Removed the bare one in the `changes` job; the `test-runner-vacuous-green-guard` copy is a strict superset (same check plus the inventory artifact), needs `changes`, and is deliberately outside the path gates, so coverage is unchanged. 3. A tracked path that will not open now fails with a named violation instead of a raw ENOENT. git ls-files can list a file the working tree lacks — sparse checkout, uninitialised submodule — and the crash named neither the contract nor the surface. 4. The snapcraft hardcode is reverted and the real gap is fixed. develop already pins BUN_VERSION="1.3.14" at line 86; inlining the literal at the use site made two places to bump. The reason the PR did that is that the shell-defaults scan was gated on `.sh`, so it never read the assignment inside a YAML block scalar and called the use unproven. That gate now accepts YAML, so develop's single pin proves its own use. Separately, deploy-railway.sh wrote `FROM oven/bun:canary-alpine` into a heredoc — scanDockerfile only inspects files NAMED Dockerfile, so a floating canary shipped past a contract whose headline promise is that none survive. Added an embedded-image scan (variant suffixes like -alpine allowed, versions not) and pinned it. Verification on the rebased, develop-merged tree: contract exits 0 (368 sites, up from 363 before the widened scan), 60/60 in ci-bun-version-contract.test.ts. Five new tests cover each finding, and the headline one is mutation-checked — restoring the old precondition fails "scans a non-.github workflow whose setup-bun wires no version". The unreadable-path test builds a real git checkout, since a synthetic tree uses the directory-walk fallback and cannot express a tracked-but- absent file. Biome clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011gZy3feiGVKs61PDUzx1Cd --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(cloud): scope durable onboarding admissions * fix(cloud): harden onboarding durable replay cleanup * fix(cloud): bound onboarding replay cleanup * test(cloud): prove onboarding tenant isolation
Co-authored-by: lalalune <18633264+lalalune@users.noreply.github.com>
* fix(agent): bound what a snapshot capture materializes in the agent's own heap Addresses review point 1 on elizaOS#17180 (elizaOS#17172 section 1). The Cloud restorable-size check is strictly downstream of this process having already assembled AND serialized the whole payload, so it bounds what Cloud retains, never what the agent's heap burns getting there. createAgentSnapshot ran five captures concurrently with no size awareness at all: file capture base64-encodes whole files (~2.33x the file size transiently) and the media set has no include predicate, so a large agent could exhaust its container mid-capture. The memory watchdog then force-restarts it, and the lifecycle sites that snapshot before upgrade/shutdown/sleep degrade to a stale or missing backup. Adds a produce-side budget charged as bytes are produced, so an over-budget capture is refused at the first byte past the line instead of after everything is resident. File entries are refused from the declared stat size BEFORE the read, since charging afterwards is charging after the allocation. An optional AbortSignal is threaded through the directory walk so a cancelled capture stops descending. The default ceiling is the shared canonical restorable limit, so the producer refuses exactly what the consumer would. Deliberately NOT claimed as complete: charging the Postgres rows bounds the assembled snapshot, not the peak of the single query that produced it — pool.query buffers the whole result set and node-postgres cannot interrupt an in-flight SELECT. Bounding that peak needs batched keyset reads or a cursor dependency in the agent image; that is a maintainer call, called out in the code and the PR rather than papered over. Stacked on elizaOS#17180, which introduces the shared limits module this imports. Tests: 9/9 in packages/agent, including a red-control that the oversized file is never read. Refs elizaOS#17172 section 1. * fix(agent): read agent-scoped Postgres tables in keyset batches Closes the limitation the previous commit documented instead of solving: charging the serialized rows bounded the assembled snapshot, but pool.query buffers whatever a statement returns, so an unbounded SELECT * WHERE agent_id = $1 still put an entire table in this process's heap before the budget could see a byte. On a chat-heavy agent the memories/embeddings/logs tables are the gigabytes. The two unbounded shapes (the memories-joined embeddings read and the generic owner-column read) now walk in keyset batches ordered by primary key, charging the budget per batch, so the peak is one batch and an oversized table is refused mid-walk. Keyset rather than OFFSET: OFFSET re-scans from the start each page, and a snapshot taken while the agent is live would skip or duplicate rows as they shift. The agent row is already bounded by construction (WHERE id = $1), and a table with no primary key keeps its single read with the post-read charge — documented where it happens. No new dependency: this is the keyset option, not the pg-cursor one that would have needed a call about the agent container image. Tests: 12/12, including that the walk covers a 1200-row table with no duplicates or gaps, that every statement is ORDER BY + LIMIT and resumes from the last id, and that an over-budget table stops early instead of reading to the end. Refs elizaOS#17172 section 1. * fix(agent): make the snapshot budget reserve capacity and cover every capture path Two holes from review. First, reserve() observed the projected total without holding anything, so concurrent captures could each pass the same check, read and encode in parallel, and only collide at charge time - after every payload was already resident. reserve() now returns a settle-once token that claims capacity other reserves must count; the actual encoded size is charged on commit, and a failed read releases the hold. Second, the PGlite dump path materialized the whole dump (Blob, ArrayBuffer, Buffer, base64) without consulting the budget at all, and the pglite-files fallback walk plus the character config read were equally unbudgeted. The dump is now refused from Blob.size before arrayBuffer(), and every component of a capture draws from the one budget. A refusal in one component now also aborts the others: the capture owns an internal AbortController wired into the shared budget signal, so siblings stop at their next check instead of reading at full speed toward a result that is already doomed. The keyset-capture doc no longer implies transactional consistency; it bounds memory, and says exactly that. * fix(backups): close snapshot safety gaps --------- Co-authored-by: standujar <s.andujar@proton.me> Co-authored-by: moon <autonomousresearcher@gmail.com>
…lizaOS#17541) * registry: add 4 x402 third-party plugins (concordance, trading-strategy-data, trustfetch, chainscope) * registry: bump 4 x402 third-party plugins to 0.2.0 (real x402 payment fix) * registry: bump 4 x402 third-party plugins to 0.3.0 (client-side payment policy fix) Addresses the review on the prior PR: all 4 clients signed whatever amount/network/asset/payTo a 402 challenge specified, with no local ceiling, expected-recipient check, or asset/network pin. 0.3.0 adds a local policy check before createPaymentHeader is ever called (exact scheme/network/asset/payTo match plus a configurable per-request max amount), with adversarial tests proving no signature is produced when any of those fields is tampered with. * registry: bump 4 x402 third-party plugins to 0.3.1 (fix source/npm mismatch + dependency audit) 0.3.0 was published to npm from local uncommitted changes that were never pushed to the linked repos -- the review correctly couldn't reproduce the payment-policy code or the adversarial test file from the recorded gitHead. 0.3.1 is published from a commit that's actually in each repo's history (verified: fresh clone + npm install + npm test passes clean for all 4 -- 55/55 tests total). Also resolves the high-severity axios/ws transitive advisories via package.json overrides in each repo (both pulled in through x402's own dependency tree, not direct deps of the plugins) -- 0 high remaining per npm audit. --------- Co-authored-by: Vohlsyr <vohlsyr@users.noreply.github.com> Co-authored-by: Shaw <shawmakesmagic@gmail.com>
elizaOS#17933) * fix(skills): keep the gh 2.45 --jq pagination form inside the retry loop Rebased onto develop. Develop replaced gh's --slurp with `--jq .[]` because Ubuntu 24.04 ships gh 2.45, which has no --slurp; this branch's retry loop was built around the --slurp form. The loop now wraps develop's argument vector and returns parsePaginatedJson(result.stdout, endpoint) so type errors name the endpoint. The retry tests were adapted to the line-delimited output --jq .[] produces rather than slurped nested page arrays, the argv assertion now expects --jq/.[], the spawn-failure case expects develop's endpoint-context wrapper (which preserves `cause`), and the malformed-output case feeds a genuinely non-JSON line, since a bare object is valid under the new form. Develop's endpoint-context test is kept alongside the new retry coverage. * fix(skills): repair retry validation and policy * fix(skills): repair the spliced retry test block Running the suite caught two defects introduced while adapting the retry tests onto develop's `--jq .[]` output form: - develop's "fails command and spawn errors with endpoint context" lost its closing `});`, so the retry tests nested inside it and the file ended with an unbalanced block (Unexpected end of file). - three adapted `stdout` fixtures carried real newlines instead of escapes, so their single-quoted literals spanned lines (Unterminated string literal). Both fixed; the suite runs green at 23 pass / 0 fail. --------- Co-authored-by: lalalune <18633264+lalalune@users.noreply.github.com>
…gin-callsite-contract
Flows that claim the popup inside the user gesture but never reach interactive login (local-runtime finish, already-authenticated cloud provision) left the about:blank window open forever. The flow launchers now release the stash in their finally, and a re-claim closes any prior unconsumed handle before overwriting it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Test PR to trigger fork CI