Skip to content

feat(memory): integrate trust center and control session - #254

Open
bkearns wants to merge 27 commits into
mainfrom
feat/memory-trust-control-integration
Open

feat(memory): integrate trust center and control session#254
bkearns wants to merge 27 commits into
mainfrom
feat/memory-trust-control-integration

Conversation

@bkearns

@bkearns bkearns commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Executive Summary

Integrates the current Ferrosa Memory work needed trust replication.

Included

  • Durable control-session and mobile-control behavior, including bounded replay paging.
  • Trust/share-facing Memory views, bounded node-detail responses, and cellular-safe root previews.
  • Team Builder runtime specification and the canonical Trust specification pointer.
  • Search, consolidation-tenant, migration serialization, semantic vocabulary, backup, and evaluation work already in progress.

Validation

  • cargo fmt clean
  • cargo test -p ferrosa-memory-core control_store --lib — 6 passed
  • cargo test -p ferrosa-memory-sync --lib — 157 passed
  • cargo clippy -p ferrosa-memory-core -p ferrosa-memory-sync --all-targets -- -D warnings — passed

QA focus

  • Trust Center navigation and sharing flows.
  • First-load/cellular memory listing and node drill-in.
  • Control-session reconnect, replay, and activity behavior.

bkearns added 27 commits August 20, 2026 16:10
…ll queries

Three changes to fix cross-domain pollution in ferrosa-memory retrieval:

1. Workspace routing in auto fusion profile selector (dispatch.rs):
   - When workspace_cwd is present, route to bm25-semantic-workspace
     profile (workspace_weight=2.0) instead of bare auto/bm25-semantic
     profiles that zero workspace_weight
   - Fixes: searches from one repo returning unrelated entities from
     another repo (e.g. project-A query returning project-B entities)

2. External fusion weight tuning via [search.fusion] config (config.rs):
   - New FusionTuningConfig with Option<f64> overrides for all 18 weights
   - Applied after profile selection, before per-call fusion_weights
   - Lets operators set workspace_weight=2.0 in config without recompiling
   - Defaults are None (no override, preserve profile behavior)

3. Recall hook query shaping + ingest project linking (turn-hook.py):
   - extract_focus_query: first sentence/clause (max 200 chars) instead
     of full 4000-char prompt as search query
   - query_decomposition=heuristic for auto query variant generation
   - Ingest creates project entity + learned_in edge for every turn,
     giving the knowledge graph workspace structure for workspace_weight
     to boost
   - Fix Python 3.9 compat: zip(strict=False) and isinstance(int|float)

Tests: 15/15 fusion tests pass (12 original + 3 new regression tests)
Verified live: workspace_weight=2.0 visible in fusion weights when cwd
is provided; project entity + learned_in edge confirmed via search
…ller

The ferrosa-memory installer now installs three Codex-side files that
improve ferrosa-memory usage quality:

1. forge-first.sh (PreToolUse/Bash hook): nudges agents toward Forge MCP
   tools (project_summary, test_summary, cargo, git_summary, etc.) instead
   of raw shell output that wastes tokens. Soft mode by default; hard-block
   mode via CODEX_FORGE_FIRST_BLOCK=1.

2. agents-template.md (AGENTS.md): a Forge-first decision table mapping 17
   common repo tasks to their Forge tool equivalents. Appended to existing
   AGENTS.md if present; created if absent. Idempotent on re-runs.

3. install-agent-hooks.py changes: patch_codex now calls
   install_forge_first_hook (copies script + registers PreToolUse entry)
   and install_agents_template (writes/appends AGENTS.md).

All three are installed only for Codex (the harness with PreToolUse support).
Claude/Hermes/Goose/Pi skip these since they don't have PreToolUse hooks.
Format & Lint gates every other job on this PR, so both were invisible until
it passed.

- cargo fmt diffs in dispatch.rs
- clippy derivable_impls: FusionTuningConfig hand-wrote a Default that set
  every field to None, which is exactly what the derive produces

Only these two fixes. The fusion-tuning and semantic-name-lint work in the
working tree is deliberately left out for a separate PR.
…ts, so clients read it as "none"

`tools/list` serves the compact catalog: name, category, summary and a schema
digest, with no inputSchema. That is deliberate — it is how 100+ tools fit in a
token budget, and `detail=schema` is how a client gets the rest.

But it also said nothing about which arguments are mandatory, and an absent
`required` is read by clients as an empty one. A caller that trusted the
listing called `ingest` without `content` and got

    -32602 "missing required string: content"

an error the listing said could not happen. The call fails at STORE time, so
every retrieval check after it returns zero — for a reason that has nothing to
do with retrieval.

Compact entries now carry the `required` names. Names only: no types, no
descriptions, no properties. That is a handful of tokens per tool and it is the
difference between a listing a client can call from and one it cannot. The
existing page-budget test still passes.

Always an array, never a missing key. "I was not told" and "nothing is
mandatory" are different answers and a caller cannot tell them apart from an
absent field, so every entry answers the question — including the tools whose
answer is none.

Note on QA-0009, which this supersedes: `entity_type` IS still required by
smart_ingest, alongside `content`. The row was raised when a documented example
omitted it. Both now appear in the listing, so following the listing works
whichever field the caller forgets.
…atalog-required

# Conflicts:
#	crates/ferrosa-memory-core/src/auth.rs
#	crates/ferrosa-memory-core/src/control_store.rs
#	crates/ferrosa-memory-core/src/dispatch.rs
#	crates/ferrosa-memory-mcp/src/main.rs
#	crates/ferrosa-memory-sync/src/control_session.rs
A team is a graph the user draws -- agents as nodes, edges saying when
one agent involves another -- and the first team is researcher, writer
and reviewer producing a knowledge claim for human review.

Four decisions were settled before design, because each changes a schema
or a trust boundary rather than an interface, and answering them
afterwards would mean rebuilding. ADR-009 records them: the runtime is
first-class and server-side so a run outlives the client that started it;
delegation goes through one send_to(agent, request) endpoint whose target
the engine authorises against the graph; node capabilities are declared
now and enforced later, with the gap shown rather than implied; and an
exhausted review cycle parks and asks a human rather than publishing or
discarding.

The design doc works out what those force. One endpoint plus forwarder
nodes means the graph has cycles by design, so termination is never
structural -- five explicit bounds, including a no-progress bound,
because the other four can all be satisfied while two agents exchange
messages politely and the draft never changes.

Two things the threat model says that are easy to assume otherwise.
Denying the writer internet stops the writer ACTING on the internet; it
does not stop web content REACHING it, since that is exactly what the
researcher returns -- isolation bounds action, not information, so
researched content is untrusted data and never instructions. And a
forwarder path is capability laundering only if the graph permits it: the
graph IS the policy, so the editor must show reachability rather than
direct edges alone.

Provenance is recorded by the runtime from real message traffic rather
than asserted by the writer, and writer-reviewer agreement produces a
candidate rather than a truth -- the same rule ontology-from-observations
states for induced ontologies. "Ready for review" is load-bearing.

Eight slices, ordered by what unblocks what, with the pure core of each
called out: authorisation, bounds, state transitions and provenance
assembly are all testable without a cluster, a browser or a model.
Confirms the reading the blueprint had inferred, and answers the open
question it left against slice 6.

Agent teams send every artifact to the responsible human before it is
approved. That is now Decision 5 rather than something implied by the
phrase "ready for review", and it is structural: there is no transition
from any agent-produced state to approved. Agreement between writer and
reviewer buys a better starting state, never the outcome.

A parked run's draft does not wait inside the run. It enters the queue
immediately as `blocked on human`, carrying the objection. This is better
than what the design had: a draft held inside a parked run is work nobody
can see until they open the run, while in the queue it is work with a
state. So both endings reach one destination and differ only in the state
they arrive in and what they carry.

"Responsible human" is an assignment, not a pool, which adds a failure
mode worth refusing outright: a claim that cannot name an owner is a
claim nobody reviews, and it would sit in the queue looking like
progress.

T6 gets stronger as a result. Claim laundering was mitigated by entering
the queue in a proposed state -- procedural, and only as good as whoever
reads the badge. It is now mitigated by there being no path to approved
that does not pass through a person.
Four more constraints, and two of them interact.

A graph that cannot terminate is now INVALID rather than risky. Three
ways to have a terminal state -- exhausted attempts, elapsed time, or a
reachable completion exit -- and the editor refuses to save a team
without one. Because cycles are legal, a reachable exit is not implied by
the shape of the graph, so it has to be checked. The check is a pure
function of graph plus bounds.

The definition is locked at creation and a run pins it, so no run changes
shape underneath itself and a transcript can always be read against the
graph that produced it. Occupancy is deliberately NOT locked: a teammate
may be swapped while the team is paused and that teammate is idle.
Swapping mid-turn would orphan a reply with nowhere to land, so the idle
condition is a refusal rather than a warning. The consequence is that
provenance must record which occupant produced which turn -- "the writer
said this" stops naming one agent the moment a swap is allowed.

Every teammate has a session a human can enter, which is what makes pause
useful rather than merely stopped: pause, ask the reviewer what it
actually objects to, then resume or swap. Human turns are attributed to
the human, because an unmarked one lets a person's paragraph ship as
agent-team output -- the inverse of the laundering Decision 5 prevents.
They also spend budget, because the tokens are real.

Pause and stop are separate controls and both are WRITTEN. Held in
memory, either is undone by a restart, and a run that resumes because a
process bounced has ignored the person who stopped it. Both show every
draft in flight on the active team page -- every draft, not the newest,
which makes team_artifact append-only. Someone reaching for pause is
usually asking what the thing looked like before the last exchange, and a
run that overwrites drafts cannot answer.

Two open questions resolved, two new ones raised: whether a STOPPED run's
drafts go to the queue for salvage or stay with the run, and whether
entering a session pauses the team automatically.
Corrects the previous commit, which had pause halting the agents as well
as the traffic. It does not.

A paused run still has agents working. What stops is DELIVERY: a message
an agent sends is persisted and held, and goes out when the run starts
again. Nothing is refused and nothing is lost.

Two consequences the interface has to carry, because the word "pause"
implies otherwise. PAUSE DOES NOT STOP SPEND -- an agent mid-turn keeps
thinking and keeps costing, so someone pausing to halt a runaway bill has
reached for the wrong control. And resuming delivers a backlog, so a run
can get busier the moment it resumes than it was when paused.

Pausing an AGENT is a different mechanism at a different level: enter its
session and use the harness's interrupt. Three controls, not one with
flags.

Messages address a ROLE rather than an agent, which settles what happens
to messages queued for a teammate that is swapped while paused: the new
occupant inherits them. That is the point of swapping -- replace a
struggling reviewer and it picks up the queue.

The graph shows three node states, not two: green working, orange waiting
for input, grey dead. Waiting and dead both look like silence from
outside and only one of them wants a human, so `dead` must be established
by the runtime -- a crashed harness, a lost session -- and never inferred
from a gap in traffic, or a slow agent becomes a dead one on a busy day.

Stopped runs send their drafts to the queue as well. Stopping ends a run;
it does not discard the most expensive thing the run made. A stopped
draft awaits a decision rather than approval: archive, trash, or send on
to another agent or team for fine tuning. Send-on makes an artifact
something teams pass between them.

That leaves a question worth answering before slice 6: does a sent-on
draft carry its provenance? Carrying it preserves the audit trail and
risks a claim citing work its final author never saw.
…doff

Two global controls that behave unlike the ordinary ones.

HALT ALL stops execution everywhere -- not delivery, execution. This is
the control pause is repeatedly mistaken for, and the one that actually
stops the bill. It answers a failure mode the previous commit could only
name: pause looks like a spend brake and is not, and until now nothing
was.

KILL ends a run and discards its work. The difference from stop is the
point and must be visible: a stopped run sends its drafts to the queue
for archive, trash or send-on, and a killed run does not. Kill exists for
work that must not be kept -- bad data, a prompt injection that took,
output nobody should act on -- and if it merely stopped, that work would
be sitting in the queue waiting to be used.

Three rules apply to both and to nothing else. They are WRITTEN BEFORE
they act, because a break-glass control living in a process is worthless
exactly when it is needed: whatever made someone reach for it may be what
restarts the process. Release is EXPLICIT -- nothing resumes on a timer
or because a process came back, since someone halted everything on
purpose and the system does not get to decide when that purpose expired.
And THE RECORD OUTLIVES WHAT THEY DESTROY: kill discards drafts, never
the fact that a run existed and was killed, or a killed run and one that
never happened would look identical, and the most consequential action in
the system would leave the least evidence.

Separately, the open question on send-on is answered: a draft carries
where it came from. The nuance that makes it safe is that provenance is
attributed BY ORIGIN rather than absorbed -- inherited evidence renders
as inherited, naming the team and run that gathered it. Absorbed
provenance would let a receiving team cite sources indistinguishable from
its own work and would end the audit trail at the one point it most needs
to continue.
Three levels: one team, every team a user manages, and an organisation.
Org scope arrives with human teams collaborating with agent teams, so it
is not built first -- but the scope field is in the model from the start,
because retrofitting a scope onto a boolean means revisiting every read
of it.

Scoping only works if three properties hold, and each rules out a way the
control could be quietly defeated.

HOLDS STACK. A team may be held at team, user and org level at once, and
execution requires zero holds covering it. A single flag would let the
narrowest release undo the broadest -- release the team hold and resume
work an organisation stopped.

RELEASE AUTHORITY MUST MATCH OR EXCEED THE HOLD'S SCOPE. A user cannot
release an org halt. Without it the org control is advisory, and an
advisory break-glass control is not one.

A HALTED TEAM NAMES THE HOLD AND WHO SET IT. "Halted" alone sends a
person to a button that will refuse them; "halted by your organisation"
sends them to a person. The control either explains itself or looks
broken.

Kill stays unscoped by contrast: it names one run, because a control that
destroys work should never take a wildcard.

Storage follows the shape -- halt_hold is one row per active hold, and a
release deletes one row rather than clearing a flag.
A managed sandbox can often be suspended and resumed -- frozen
mid-execution and continued where it stopped. Most backends can; not all.
So suspend is declared per backend, exactly like the node capabilities in
Decision 3 and for the same reason: never present a guarantee the
substrate does not provide.

Halt therefore has two behaviours. On a suspend-capable backend it
freezes execution and resume continues the turn. Without suspend the turn
in flight is abandoned and resume restarts it from its last durable
point. The run states which it will get BEFORE someone reaches for the
control, because "this will freeze and continue" and "this will discard
the turn in progress" are different enough decisions that guessing is not
acceptable.

This also qualifies a claim made two commits ago. "Halt stops the bill"
is only fully true with suspend: a suspended sandbox stops consuming,
while an abandoned turn has already spent what it spent and restarting it
spends again. Without suspend, halt bounds future cost rather than
eliminating it.

And resume cannot assume the world stood still. A sandbox frozen for an
hour wakes to timed-out connections, possibly expired credentials, and a
memory server that may have restarted. Resume re-establishes what it
depends on and fails loudly if it cannot, rather than continuing against
a stale handle -- the same class of fault as holding peer addresses
frozen at startup.
Amends Decision 1 rather than replacing it. Memory still owns the state,
the authorisation and the record -- that argument was always about
durability of state, not about where a process runs. What moves out is
EXECUTION: coordinating sandboxes and VMs happens in a separate installed
process shaped like the streamer, supervised and signed, responsible for
the machines it can reach.

That splits along a line this project already draws. Decisions about who
may do what stay in the audited half; platform mechanics do not. It is
the same arrangement as the streamer, where the public side owns the
channel and the identity and the private side owns capture -- so a
coordinator may be private without any authorisation leaving the audited
half.

Coordinators NEST. One per host, and a delegate in the cloud that is
itself a coordinator for the machines below it. The topology is a tree
and "the coordinator" is a role, not a singleton.

That breaks a guarantee Decision 11 relied on. A halt written before it
acts is sufficient in one process. Across a tree it is not: a delegate
that cannot reach the database cannot see the halt, and a coordinator
that cannot see a halt is one still running agents -- a hole in break
glass exactly where nobody can look.

So every coordinator executes on a LEASE, renewed against the database,
and stops by itself when renewal fails. A halt is then enforced by
ABSENCE rather than delivery: cutting a coordinator off is as effective
as telling it to stop, which inverts the usual failure where losing
contact means losing control. The cost is that a database outage stops
agent work, and for a system that spends money and touches the internet
that is the correct direction to fail.

It also sharpens "survives the laptop closing". Run STATE always
survives; a cloud delegate keeps executing; sandboxes on a laptop stop
when the laptop does and resume when their coordinator returns.
…hey provision

Coordinators reach the database and the agents over the same WebRTC
control channel the streamer uses, so they are peers on that plane and do
not need to sit beside a database node. What they do need is direct
control of whatever runs sandboxes: cloud credentials, or host access to
the container runtime.

That inverts the obvious adjacency requirement. Near the runtime and far
from the database is fine; the reverse is useless, because a coordinator
with a perfect database link and no way to start a container coordinates
nothing.

It also puts the lease behind that transport, and its failure modes are
not hypothetical here -- ICE failing mid-session with throughput at zero
and no packets reported lost, inbound UDP dropped by a host firewall so
it fails only on the local network, a TLS expectation mismatch that hangs
rather than refusing. Each expires a lease and stops a coordinator, which
is the correct direction, and it makes lease duration a real decision:
too short and an ordinary reconnect stops agent work, too long and a
coordinator that has been cut off keeps spending. Whatever is chosen, an
expiry must render AS an expiry -- that class of failure has previously
surfaced in this system as "the display is blank" and "the stream
stalled".

The sharpest consequence is a security one, and nothing else in the
design mitigates it. send_to authorisation is audited, but it governs
which agent may ask which other agent for work; it says nothing about
what a COORDINATOR may do, because the coordinator is downstream of that
decision and executes what was already authorised. A container socket is
root on that host. Cloud credentials are code execution and spend.
Compromise one and the graph's authorisation is irrelevant.

So: credentials scoped to provisioning rather than general-purpose,
privilege declared and visible the way node capabilities are, and actions
recorded where the coordinator cannot edit them.
…ission

Default 15 minutes, configurable behind a granted permission.

The number states a guarantee rather than a preference: a coordinator
that loses contact -- partitioned, halted, or compromised -- keeps
working for at most fifteen minutes. Long enough that ICE renegotiating
or a laptop changing networks does not stop agent work; short enough that
a runaway is bounded to a quarter hour of spend after anyone notices. The
trade is linear, which is why it is not a free knob: doubling the lease
doubles the worst-case spend after a cut-off.

It is a permission because the two problems that motivate changing it
push in opposite directions. Partitioning wants it longer and pays in a
longer window of uncontrolled execution; runaway spend wants it shorter
and pays by turning ordinary reconnects into outages. Neither adjustment
is routine and both weaken something, so the permission exists to make
the change deliberate and attributable. The record says which problem it
was for, so a lease lengthened for a partition fixed last month is
visible as exactly that.

The part that keeps this honest: THE CEILING IS NOT ADJUSTABLE BY THE
PERMISSION THAT ADJUSTS THE LEASE. Unbounded, this is not a tuning
permission at all -- it is a permission to disable break glass, since a
day-long lease means a cut-off coordinator runs for a day. Raising the
ceiling is separate and higher-scoped, the way releasing an org halt
requires org authority.

A floor matters in the other direction. A lease below normal reconnect
time makes every network hiccup an outage, and that is an easy place to
land immediately after a spend scare.
The previous commit had the lease moving both directions, but framed both
as reactions to something going wrong -- partitioning and runaway spend
-- and said neither adjustment is routine. Maintenance breaks that
framing. It is planned, and it is common.

Rolling the memory cluster, restarting database nodes, replacing a
coordinator: during that work the control channel is EXPECTED to drop,
and with a fifteen-minute lease every coordinator expires and stops
partway through. Routine maintenance would become a full stop of agent
work. Rolling this cluster is normal operating practice here, so this is
an operating procedure rather than an edge case.

That argues for something the previous commit did not have: an override
CARRIES ITS OWN EXPIRY. A maintenance bump states how long it applies and
reverts to the default by itself.

This is the actual mechanism behind "I do not want this happening often".
A permanent bump quietly becomes the new normal -- a lease raised for a
partition fixed last month, or a window that closed in August, is
indistinguishable from deliberate policy until somebody audits it. An
override that expires cannot rot that way, and reverting stops being
something a person has to remember.

Match the window to the work. Four hours of weakened break glass for
twenty minutes of maintenance is three and a half hours of nothing
gained.
…sword

A coordinator needs root to finish provisioning, root needs a password,
and the password belongs to a person. expect and tmux can both drive an
interactive sudo, and they differ on the only question that decides it:
who holds the password.

With tmux the operator types into a live PTY the coordinator spawned.
Nothing between the keyboard and sudo ever holds the secret -- no store
to protect, no argument list to leak it, no retention question. With
expect the script must HAVE the password in order to send it, which means
it was transported, held in memory, and possibly logged: a materially
worse position for the same outcome.

It also extends what the app's probe already established -- "sudo uses
the remote PTY for its prompt; the app never pipes or records it" -- and
makes that PTY persistent and attendable, which is what the per-teammate
sessions already are. The elevated shell is the same shape as every other
session rather than a special case.

Two constraints follow. The transcript must not record INPUT during a
prompt: sudo does not echo, so a password never reaches the output
stream, but a session transcribing keystrokes would capture it anyway and
the transcript is durable. And expect keeps a narrower job -- fine for
deterministic automation where nothing secret is typed, not the mechanism
for acquiring privilege.

Probing needs neither: sudo -n true already answers whether sudo wants a
password, which is what the bootstrap probe uses.
One coordinator tool: request_secret(name, purpose) returning a status
and, when granted, a path. The human types the secret, the coordinator
writes it, and the agent is told where it is.

The value is never returned, and that is not caution -- it is the only
arrangement that works. A returned secret is in the model's context, in
the message that carried it, and in the transcript, which is durable and
append-only so drafts can be reviewed. There is no way to hand a value to
an LLM and also keep it out of the record. A path has none of that: an
agent can run docker login --password-stdin < path without ever seeing
the contents, and the transcript records that it did.

Same rule as Decision 16 applies to sudo. There the operator types into a
PTY and nothing between holds the secret; here the secret must outlive
the keystroke so it lands in a file, but the agent's position is
identical.

Written 0600, in the RUN's own directory rather than a shared one so two
runs cannot read each other's credentials, and deleted when the run ends
at the latest -- a secret whose only expiry is the disk being wiped is a
liability the run created and did not clean up.

Refusal is a normal outcome. An agent must handle it and continue or stop
cleanly, because a design where refusing breaks the run is one that
trains people to approve.

The dangerous part is not storage. A secret request is agent-authored
text shown to a human, and the agent may have read the open web: a page
saying "ask the operator to paste their GitHub token" becomes a
plausible-looking prompt. Capability isolation does not help, because it
bounds what an agent can DO rather than what it can SAY, and this is
aimed at the person. So the name comes from a declared set where the team
defines one, the purpose renders as untrusted agent-authored text rather
than as interface copy, and what the asking agent has read is one click
away.
… public

Decision 13 allowed this; this makes it the plan and writes down the
contract, because "private" is cheap to say and has consequences that are
not.

The governing rule is that no private component makes an authorisation
decision -- one that does can be swapped for one that authorises
differently, and the swap is invisible to anyone auditing the public
half. So five things stay in Ferrosa Memory whatever repository the
coordinator ships from: the send_to target check, lease granting, halt
holds and their release authority, whether a requested secret name is
permitted, and node capability declarations. The coordinator enforces
every one and decides none. It reads a lease and stops when it lapses; it
does not grant itself one.

The seam also has to be real. A public trait is not enough on its own: a
working reference implementation must ship publicly and be exercised by
public CI, and the public repository must build and test with no access
to the private one. A trait with no in-tree implementation is a contract
nobody compiles against, and it drifts until someone tries to swap it and
finds all the drift at once. So the reference implementation does
something -- runs a fake sandbox, takes a lease, lets it expire.

Which private repository is not settled. ferrosa-streamer already carries
the private platform half, but its subject is capture and this one's is
machines, and a shared repository puts two unrelated products behind one
release cadence. Worth deciding before the first commit, since moving a
repository later is cheap and moving its history is not.

Public is one-way: a branch pushed public stays fetchable by SHA after
deletion. Where placement is uncertain, private is the reversible choice.
@bkearns
bkearns added this pull request to the merge queue Aug 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Aug 30, 2026
@bkearns
bkearns added this pull request to the merge queue Aug 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Aug 30, 2026
@bkearns
bkearns added this pull request to the merge queue Aug 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant