Skip to content

fix(llm-agents): free the canvas bottom overlay before the memory inspect click (#1643) - #1674

Open
Victor-w-Madeira wants to merge 7 commits into
mainfrom
fix/issue-1643-canvas-bottom-overlay-intercept
Open

fix(llm-agents): free the canvas bottom overlay before the memory inspect click (#1643)#1674
Victor-w-Madeira wants to merge 7 commits into
mainfrom
fix/issue-1643-canvas-bottom-overlay-intercept

Conversation

@Victor-w-Madeira

@Victor-w-Madeira Victor-w-Madeira commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Closes #1643.

Problem

Both context-id retrieval specs hard-failed all 3 attempts of the 2026-08-31 daily (run 33410643882, triage #1642) on a 20 s locator.click timeout against output-inspection-messages-memory — the button resolving as visible, enabled and stable and every retry refused by <div class="absolute bottom-16 left-1/2 z-50 w-[530px]">… subtree intercepts pointer events. Quarantined by hand in #1647 because the mass-failure guard tripped and the workflow auto-removed nothing.

Root cause (test defect — confirmed live, not a product regression). Langflow renders two different components into one fixed container over the canvas, absolute bottom-16 left-1/2 z-50 w-[530px] -translate-x-1/2:

Component Behaviour in the slot
flowBuildingComponent — the build-status bar transient on success: handleDismiss fires 2 s after "Flow built successfully"
UpdateAllComponents — the "Flow needs review / N components need updates" banner its shouldHide includes buildInfo?.success, so it is hidden while the bar is up, retakes the slot when the bar's state is cleared, and then never leaves

Measured at the default 1280×720 viewport, the inspect button's box is y 585.6–601.0, centre 593.3. The build bar's top edge is y 598 — the click clears it by ~5 px. The banner is 12 px taller, top edge y ~586above the centre. So the identical click passed or was refused purely on which component owned the slot, and once the banner owned it no amount of retrying could help, which is exactly what the call log shows: first interceptor the bar's inner flex min-h-10 w-full items-center justify-between gap-2, second the banner's flex items-center justify-between gap-6 rounded-lg border bg-background px-4 py-3 text-sm shadow-md.

The banner is present because these specs seed their flow from tests/assets/flows/chat-io-ok-trace-fixture.json, whose nodes carry lf_version: 1.7.0; the nightly reports one of them as outdated.

Reproduced deterministically: dwelling 4 s before the click hands the slot to the banner, and the failure returns with the interceptor byte-identical to the daily's. With the fix the same dwell passes — probe reads Flow needs review …EMPTY.

The environmental hypothesis is ruled out as the issue asked: this reproduces on an idle local nightly with no wedge, and the daily's own liveness recorder had both specs' final attempt at 0–3 % of probes down.

Fix

New shared helper tests/helpers/ui/clear-canvas-bottom-overlay.ts, called between the run and the inspect click. It frees the slot instead of clicking into it, and fails closed in three directions (the second and third came out of review):

  • Timeout → throws naming what is still in the slot and whether Dismiss was tried, so a build that stops honouring its own Dismiss fails attributed rather than as an unexplained locator.click timeout at the call site.
  • Lost selector → a slot that was never occupied is an error, not success. Callers reach this right after waitForSelector("text=built successfully"), which proves the bar is on screen, so "nothing ever matched" can only mean an upstream Tailwind edit (w-[530px]w-[560px]). Reporting "clear" there would bring [Daily #1642] agent-context-id-{isolation,continuity} — an overlay at the canvas bottom intercepts the memory-output inspect click, and mode: 'serial' skips each file's sibling #1643 back as precisely the unattributed timeout the helper exists to prevent — with a helper call in the trace implying otherwise. { allowAlreadyClear: true } opts out.
  • Failed-build bar → refused by name, never dismissed. It renders Retry + Dismiss and has no timer, so it satisfies the "will not leave on its own" predicate — but dismissing it erases the only UI evidence of a failed run, and flow-error-policy's v2 verdict is advisory on 1.12.x, so a caller could go green on a build that failed.

The empty-slot window is sized against the real constant, not the observed gap. handleDismiss sets dismissed = true (framer exit ~340 ms) and separately schedules setBuildInfo(null) at +500 ms, which is what un-hides the banner — so the gap is 500 ms − exit, measured ~89 ms with a 10 ms sampler. Two 200 ms-spaced reads cleared that by ~2.2×, but drop the exit animation upstream and the window fits inside the gap. EMPTY_CONFIRMATIONS = 5 (an 800 ms quiet window), and since the fake counts ticks and ignores durations (a POLL_MS = 0 survived every behavioural test), the relationship POLL_MS × (EMPTY_CONFIRMATIONS − 1) > BANNER_UNHIDE_DELAY_MS is asserted on the constants.

Also documented, because it was not obvious: the helper writes to the flow. handleDismissAllComponents calls setNodes marking each flagged node edited: true, and the editor persists it with a PATCH /api/v1/flows/{id} (observed on the wire). Harmless for a caller that seeds a throwaway flow and deletes it in teardown; a trap for one that asserts on the persisted graph.

Rejected alternatives: refreshing chat-io-ok-trace-fixture.json (shared by many specs, silences this only until the next template bump, leaves the build bar's ~5 px margin as fragile); click({ force: true }) (force skips the actionability check but the browser still dispatches at the point — the overlay swallows it and the test fails later, worse attributed); panning/zooming the node out of the slot (trades one geometric coincidence for another).

Third file: agent-n-messages-limit.spec.ts — hardening, and the distinction is measured

Same fixture, same node, same five lines, both tests @stable; #989 treated the three sibling files together. But it is not currently intercepted, and that was checked rather than assumed. The banner is present on its flow (1.13.0.dev0, slot y 586–656) — what differs is the margin: this node exposes two advanced fields where the context-id specs expose three, so it is shorter and its inspect button sits at y 541.0–556.9, ~37 px clear against ~5 px. Forcing the failing condition does not reproduce an interception there. Added anyway because 37 px is a coincidence of node height and node height is exactly what upstream moves (dev46 already changed how these fields are exposed); it costs ~2 s and removes the dependence.

Four further specs share the exposure in a different shape (split-text-chunking, and byte-identical private dismissUpdateBannerIfPresent copies in rag-pipeline / vector-store-index-query, plus a bare waitForTimeout(600) in chatInputOutputUser-shard-1). Folding those into the helper is a dedup refactor, not this fix — follow-up issue.

Scope note

No assertion changed anywhere. What each test proves is untouched; only the path to the output inspector is. The parametrized agent tests in these files are unchanged.

All three files stay mode: "serial" (the agent-area rule). With the retrieval tests passing, each file's sibling runs again — the 2 skips this cluster caused are gone, verified from the runner. The coupling itself is already mitigated the way these files can mitigate it: the model-free describe is declared before the parametrized loop, so a weak-model failure cannot skip the half that needs no provider. That ordering cannot help when the failure is in the first describe, which is what happened here.

Validation

Run on nightly 1.13.0.dev0langflowai/langflow-nightly:latest cut over to the 1.13 cycle on 2026-09-02T03:54Z, and that is the image daily-stable.yml pulls, so restoring @stable on a 1.12-only validation would ship these into a lane running an image they were never run against. Root cause and geometry were measured on 1.12.0.dev39.

  • npm run typecheck ✅ · npm run lint ✅ (0 errors) · npm run test:units ✅ (893 pass / 0 fail, incl. 9 new) · npm run check:checklist-coverage ✅ · check-checklist-guard.mjs ✅ · watch-upstream-areas --mode=check-docs ✅ (no unresolved path in the changed docs)
  • 2 clean full-file runs on 1.13.0.dev0 of both context-id specs: 6 passed, 2 skipped (1.6–3.8 min). 2 clean runs of agent-n-messages-limit: 2 passed (~18 s).
  • 3 further clean full-file runs on 1.12.0.dev39 plus 3 isolated --retries=0 runs of the isolation retrieval test.
  • The 2 skips are the anthropic parametrized targets — providers.json records the provider inactive ("Your credit balance is too low to access the Anthropic API"), a pre-existing key state unrelated to this issue. openai and google pass.
  • Adversarial: the deterministic repro (4 s dwell, banner owning the slot) fails on main and passes with the fix.
  • The green run now proves the selector too — with the fail-closed guard, a run that passes cannot have silently skipped a present overlay.
  • Zero 🚨 Backend Error on the final runs. (An earlier 1.12 run logged one 404 on GET /api/v1/flows/{id} — that is resolveLiveFlowId probing the collected transient ids on purpose, documented in the spec.)
  • Flow-orphan check: the fresh 1.13 instance holds 26 flows after every run — the starter templates only, zero leaked. On the 1.12 instance, 27 before → 27 after.
  • --trace=on not run: known limitation on the Simple Agent template family (Create agent-tool-name-validation.spec.ts — invalid tool name blocked with clear message #490/flaky/bug: export-import-flow export produces empty flow (nodes: []) — regression of #384 #518), where tracing hangs indefinitely. Covered by the --retries=0 bursts, the force-fails and live scout probes instead.

Force-fails — all EXECUTED and observed failing, then reverted (0 mutation markers left)

# Target Mutation Observed
FF-1 agent-context-id-isolation retrieval invert the cross-context negative Expected substring: "CTXISO-…-B-" ⇒ failed
FF-2 agent-context-id-continuity retrieval invert the untagged-control negative Expected substring: "CTXCONT-…-CTRL" ⇒ failed
FF-3 agent-context-id-isolation parametrized assert a context never set message(s) with wrong context_id: […] ⇒ failed
FF-4 agent-context-id-continuity parametrized same on the session assertion same shape ⇒ failed
FF-5 helper, unit EMPTY_CONFIRMATIONS 2 → 1 only the handover test fails (5 pass / 1 fail)
FF-6 helper, end-to-end pre-fix code under the deterministic repro fails with the daily's exact interceptor
FF-7 agent-n-messages-limit truncation expect 3 instead of 2 Expected: 3, Received: 2 ⇒ failed
FF-8 agent-n-messages-limit causal control expect SEEDED_MESSAGES + 1 Expected: 11, Received: 10 ⇒ failed

Related: #1642 (triage), #1647 (quarantine), #989 (the previous fix of this same three-file cluster).

…tom slot (#1643)

Langflow renders TWO components into one fixed container over the canvas —
`absolute bottom-16 left-1/2 z-50 w-[530px] -translate-x-1/2`:

- `flowBuildingComponent`, the build-status bar, transient on success (it
  auto-dismisses 2 s after "Flow built successfully", plus a 500 ms exit);
- `UpdateAllComponents`, the "Flow needs review / N components need updates"
  banner, whose `shouldHide` includes `buildInfo?.success` — so it is hidden
  while the bar is up, takes the slot back the moment the bar dismisses, and
  then stays there indefinitely.

That handover is why a plain wait cannot work, and it is what makes the helper
worth having rather than a `waitForSelector` at each call site. The predicate is
"will this occupant leave on its own?", read off the DOM as "does it offer a
Dismiss button" — exactly right for both components, since the update banner
always offers one and the build bar offers one only in its `buildInfo.error`
state, the one state where it too never leaves.

A single empty read is deliberately not enough: the slot is genuinely empty for
one render tick between the bar unmounting and the banner mounting, and
returning there hands the caller a click that races the banner. Two consecutive
empty reads are required, and that is the load-bearing behaviour the unit test
pins — reverting `EMPTY_CONFIRMATIONS` to 1 fails it.

On timeout it throws naming what is still in the slot and whether Dismiss was
tried, so a future build that stops honouring its own Dismiss fails attributed
instead of as an unexplained `locator.click` timeout at the call site.

The simulated slot in the `.fake` models the handover, the empty tick and a
banner that ignores Dismiss — none of which a live spec can dwell on. Its
contract was verified against nightly 1.12.0.dev39.
…pect click (#1643)

Both context-id retrieval helpers waited for the "built successfully" toast and
immediately clicked the Message History node's `output-inspection-messages-memory`
button. Measured on nightly 1.12.0.dev39 at the default 1280x720 viewport, that
button's box is y 585.6-601.0 — centre 593.3 — while the build-status bar's top
edge is y 598. The click cleared it by ~5 px. The "Flow needs review" banner that
replaces the bar is 12 px taller, top edge y ~586, i.e. ABOVE the centre, so the
identical click was refused for as long as the banner owned the slot — and the
banner never leaves on its own, which is why all 20 s of `locator.click` retries
were consumed on all three attempts of both specs on the 2026-08-31 daily
(run 33410643882).

Reproduced deterministically on the nightly by dwelling 4 s before the click,
which hands the slot to the banner: the failure comes back with the interceptor
byte-identical to the daily's call log —
`<div class="flex items-center justify-between gap-6 rounded-lg border
bg-background px-4 py-3 text-sm shadow-md">`. With `clearCanvasBottomOverlay` in
front of it, the same dwell passes: the banner is dismissed, the slot reads
EMPTY, and the second retrieval finds it still empty because Langflow keeps the
dismissal in `dismissedNodes`.

The banner is there because these specs seed their flow from
`tests/assets/flows/chat-io-ok-trace-fixture.json`, whose nodes carry
`lf_version: 1.7.0`; the 1.12 nightly reports one of them as outdated ("1
component needs updates", confirmed live). Refreshing that fixture was rejected
as the fix: it is shared by many specs, it would silence this only until the next
upstream template bump, and it would leave the build bar's ~5 px margin exactly
as fragile. `click({ force: true })` was rejected too — force skips the
actionability check but the browser still dispatches at the point, so the overlay
would swallow the click and the test would fail later, worse attributed.

No assertion changed: what each test proves about context-scoped retrieval is
untouched.
…th retrieval tests (#1643)

PR #1647 removed `@stable` AND added `test.fixme` by hand at the 2026-08-31
triage, because the mass-failure guard tripped (10 hard failures > 5) so the
workflow auto-removed nothing, while the day's verdict judged these two failures
non-environmental — their final attempt ran with the backend measured at 0-3% of
probes down.

With the overlay interception fixed and the cause named, both come back:

- `agent-context-id-isolation.spec.ts` — "mirrored context-scoped retrievals
  return only their own context's messages"
- `agent-context-id-continuity.spec.ts` — "context-scoped retrieval returns all
  turns of the context and not the untagged control"

Both files are `test.describe.configure({ mode: "serial" })`, so each failure had
also been skipping its file's sibling — 2 of that run's 4 genuine skips. Verified
from the runner rather than from the diff: a full-file run of both spec files is
6 passed / 2 skipped, the two skips being the anthropic parametrized targets,
which `providers.json` records as inactive on a drained key ("Your credit balance
is too low"), unrelated to this issue.
…context-id spec docs (#1643)

Adds the measured mechanism to *Step by step* (the shared container, which
component owns the slot when, the ~5 px margin and the 12 px height difference
that decides the click, and why refreshing the seeded fixture was not the fix),
names the new slot-clearing step in the retrieval sequence, records the
quarantine lift under *Tags*, and moves *Last validated* to nightly 1.12.0.dev39.
Copilot AI lite review requested due to automatic review settings September 2, 2026 04:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The fix is localized, restores quarantined coverage safely, and includes targeted unit tests that pin the previously flaky handover edge case.

Pull request overview

This PR fixes a deterministic E2E test defect where Langflow’s canvas bottom overlay intermittently (and sometimes permanently) intercepted pointer events, causing the context-id retrieval specs to hard-fail on the memory-output inspect click. The change adds a shared UI helper to proactively clear that overlay slot before clicking, lifts the prior quarantine by restoring @stable and removing test.fixme, and updates the associated spec docs accordingly.

Changes:

  • Add clearCanvasBottomOverlay(page) helper (with unit tests + a fake overlay timeline) to wait out transient overlays and dismiss persistent ones.
  • Call the helper in both context-id retrieval helpers immediately before clicking output-inspection-messages-memory.
  • Restore @stable and remove the quarantine test.fixme in both specs; update spec docs’ “Last validated” and add the overlay note.
File summaries
File Description
tests/tests-automations/regression/core-functionality/llm-agents/agent-context-id-isolation.spec.ts Uses the new overlay-clearing helper before the memory inspector click; restores @stable and removes quarantine skip.
tests/tests-automations/regression/core-functionality/llm-agents/agent-context-id-continuity.spec.ts Same overlay-clearing integration before the inspect click; restores @stable and removes quarantine skip.
tests/helpers/ui/clear-canvas-bottom-overlay.ts New UI helper that polls the shared bottom overlay slot, dismissing when possible and failing with an attributed error if it can’t clear.
tests/helpers/ui/clear-canvas-bottom-overlay.test.ts Unit coverage for the helper’s key behaviors, including the one-tick empty handover edge case and “dismiss no longer works” attribution.
tests/helpers/ui/clear-canvas-bottom-overlay.fake.ts Fake Page/Locator implementation to deterministically simulate the overlay handover timeline and non-dismissible variants.
docs/core-functionality/llm-agents/agent-context-id-isolation.md Updates “Last validated” and documents the overlay mechanism + the new required step (freeing the slot) before inspection.
docs/core-functionality/llm-agents/agent-context-id-continuity.md Same as isolation doc: updates “Last validated” and adds the overlay note + procedural step.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

…ector, a failed build and a wider handover gap (#1643)

Three defects found in review of the first version, all confirmed against the
Langflow source and a live nightly.

**It was fail-OPEN on selector drift, which contradicted its own attribution
promise.** The docstring claimed an overlay change fails attributed; that held for
a non-dismissible overlay but not for the likelier upstream edit — a Tailwind
change (`w-[530px]` -> `w-[560px]`, `bottom-16` -> `bottom-20`). The selector would
then match nothing, the helper would report the slot clear while the overlay is
fully present, and #1643 would come back as exactly the unattributed 20 s
`locator.click` timeout — now with a helper call in the trace implying it was
handled. Every caller reaches this right after `waitForSelector("text=built
successfully")`, which proves the bar is on screen, so "nothing ever matched" can
only mean the selector is lost. It now throws, with `{ allowAlreadyClear: true }`
for a future caller that may legitimately start free. The upstream files are also
declared in both spec docs' `## External dependencies`, so
`watch-upstream-areas.mjs --mode=check-docs` fails the PR that renames them.

**The empty-slot window was sized against the wrong constant.** The code said "one
render tick" and the doc said "a 500 ms exit"; neither is the mechanism.
`handleDismiss` sets `dismissed = true` — the bar leaves via a framer-motion exit
(`duration: 0.2, delay: 0.2`, ~340 ms measured) — and SEPARATELY schedules
`setBuildInfo(null)` at +500 ms, which is what un-hides the banner. The gap is
`500 ms - exitAnimation`, measured at ~89 ms on dev39 with a 10 ms sampler. Two
reads 200 ms apart cleared that by ~2.2x, but the ceiling is upstream-controlled:
drop the exit animation and the gap walks to the full 500 ms, at which point the
whole window fits inside it and the helper returns into the banner's mount.
`EMPTY_CONFIRMATIONS` is now 5 (an 800 ms quiet window) and `BANNER_UNHIDE_DELAY_MS`
records the real constant. The fake counts ticks and ignores durations, so no
behavioural test can see this — `POLL_MS = 0` survived all six — so the
relationship is asserted on the constants instead.

**The failed-build bar satisfied the "offers a Dismiss" predicate.** It renders
Retry + Dismiss and has no timer, so it is genuinely an occupant that will not
leave — but dismissing it erases the only UI evidence of a failed node run, and
`flow-error-policy`'s v2 verdict is advisory on 1.12.x, so a future caller could go
green on a build that failed. It is now refused by name. Unreachable for today's
two callers (`flowBuild.builtSuccessfully` is the only rendered "built
successfully" string, so their wait proves `buildInfo.success`) — but the guarantee
lived in the callers, not in the helper.

Also documents a side effect the first version did not: dismissing the update
banner is not read-only. `handleDismissAllComponents` calls `setNodes` marking each
flagged node `edited: true`, and the editor persists it with a
`PATCH /api/v1/flows/{id}` (observed on the wire). Harmless for a caller that seeds
a throwaway flow and deletes it in teardown; a trap for one that asserts on the
persisted graph or counts flow writes.

Unit tests go 6 -> 9.
…ate on 1.13.0.dev0 (#1643)

Three corrections from review.

`## External dependencies` now names the two frontend components the retrieval
step reads, so the repo's own rename guard covers them.

`Last validated` moves to **1.13.0.dev0**. Two reasons. The continuity doc had gone
BACKWARDS — #1187 recorded `1.12.0.dev45` four commits ago and the first version of
this PR overwrote it with the older `dev39`. And `langflowai/langflow-nightly:latest`
cut over to the 1.13 cycle on 2026-09-02T03:54Z, which is the image
`daily-stable.yml` pulls — so restoring `@stable` on a dev39-only validation would
ship these tests into a lane running an image they were never run against. Both
specs re-validated there: 2 clean `--retries=0` full-file runs, 6 passed / 2
skipped (the anthropic targets, provider `inactive` on a drained key). The overlay
mechanism is unchanged on 1.13 — and the run proves it rather than assuming it,
since the helper now fails closed when its selector matches nothing.

The `mode: "serial"` half of the issue title gets the reasoning it was missing: the
mode stays (agent-area rule), and the coupling is already mitigated by declaring
the model-free describe before the parametrized loop — an ordering that cannot help
when the failure is in the FIRST describe, which is what happened here.
…-limit too (#1643)

The third file of the cluster. It seeds from the same
`chat-io-ok-trace-fixture.json`, adds the Message History node the same way
(`addComponentFromSidebar` + `adjustScreenView({numberOfZoomOut: 0})`), and reaches
`output-inspection-messages-memory` with the same five lines the other two had —
and both of its tests are `@stable`, so they run in the daily. The #989 precedent
treated all three sibling files in one PR.

**This is hardening, not a fix of a live defect here, and the difference was
measured rather than assumed.** The banner IS present on this spec's flow too
(nightly 1.13.0.dev0, slot y 586-656, x 515-1045, "Flow needs review / 1 component
needs updates"). What differs is the margin: this node exposes two advanced fields
where the context-id specs expose three, so it is shorter and its inspect button
lands at y 541.0-556.9 — **~37 px clear of the banner's top edge**, against ~5 px
on the other two. Forcing the failing condition (dwelling 4 s so the banner owns
the slot) does NOT reproduce an interception here; it was checked, and it passed.

Added anyway because 37 px of clearance is a coincidence of node height, and node
height is exactly what upstream moves — `dev46` already changed how these fields
are exposed. The step costs ~2 s per retrieval and removes the dependence.

Validated on nightly 1.13.0.dev0: 2 clean `--retries=0` runs (2 passed, ~18 s), no
`🚨 Backend Error`. Force-fails executed on both tests — truncation asserting 3
instead of 2 fails (`Expected: 3, Received: 2`), causal control asserting
`SEEDED_MESSAGES + 1` fails (`Expected: 11, Received: 10`) — and reverted, 0
mutation markers left.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants