Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
9409b06
feat(ai-sandbox, ai-harness): codingAgents plugin to delegate to Clau…
AlemTuzlak Sep 28, 2026
d62a6cd
test(ai-opencode, ai-harness-cli): cover the failed session start eve…
AlemTuzlak Sep 28, 2026
0659520
feat(ai-harness): goal plugin that keeps working until the goal is met
AlemTuzlak Sep 28, 2026
142704c
fix(ai-harness): a plugin prompt always queues its turn
AlemTuzlak Sep 28, 2026
ce40b61
Merge remote-tracking branch 'origin/feat/harness-p9-coding-agents' i…
AlemTuzlak Sep 28, 2026
d3a80ca
fix(ai-harness): the goal plugin drops its queued turn when the goal …
AlemTuzlak Sep 28, 2026
154e0ac
test(ai-isolate-daytona): cover each timeout branch with a controlled…
AlemTuzlak Sep 28, 2026
8b46a04
feat(ai-harness): session.transcript(), session.describe(), and plugi…
AlemTuzlak Sep 28, 2026
7364ac5
feat(ai-harness): remote transcript and describe reads, client answer…
AlemTuzlak Sep 28, 2026
7779044
feat(ai-harness): session view state and its pure reducer
AlemTuzlak Sep 28, 2026
3bf4c0c
feat(ai-harness): createSessionView, a live TanStack Store view of a …
AlemTuzlak Sep 28, 2026
c902163
feat(ai-harness): selectGoal reads the goal from a session view
AlemTuzlak Sep 28, 2026
4f87a94
docs(ai-harness): build your own UI with createSessionView
AlemTuzlak Sep 28, 2026
3a53aa2
feat(ai-harness-cli)!: no UI library in the CLI; runCli({ ui }) runs …
AlemTuzlak Sep 28, 2026
9d7d383
refactor(ai-harness-cli): line mode prints from the session view; rem…
AlemTuzlak Sep 28, 2026
fb1f97e
test(ai-harness): cover the session view paths
AlemTuzlak Sep 28, 2026
7a2dd08
docs(ai-harness-cli): your own screen with runCli({ ui }), and an Ink…
AlemTuzlak Sep 28, 2026
1e1f1e9
Merge remote-tracking branch 'origin/feat/harness-p11-session-view' i…
AlemTuzlak Sep 28, 2026
281d37b
chore(examples): ts-solid-chat uses the latest @tanstack/solid-store
AlemTuzlak Sep 28, 2026
83825af
feat(ai, ai-harness): agentMiddleware runs plugin middleware in every…
AlemTuzlak Sep 28, 2026
6d97231
docs(ai-harness): middleware in every agent
AlemTuzlak Sep 28, 2026
ac2c148
Merge remote-tracking branch 'origin/feat/harness-p12-cli' into feat/…
AlemTuzlak Sep 28, 2026
47ee22f
feat(ai-harness): usage() counts the model calls of every agent
AlemTuzlak Sep 28, 2026
c782dc2
Merge branch 'feat/harness-p8-code-mode' into feat/harness-p9-coding-…
AlemTuzlak Sep 28, 2026
fa9d2ca
Merge branch 'feat/harness-p9-coding-agents' into feat/harness-p10-goal
AlemTuzlak Sep 28, 2026
556d68a
Merge branch 'feat/harness-p10-goal' into feat/harness-p11-session-view
AlemTuzlak Sep 28, 2026
7790e06
Merge branch 'feat/harness-p11-session-view' into feat/harness-p12-cli
AlemTuzlak Sep 28, 2026
a3b364b
Merge branch 'feat/harness-p12-cli' into feat/harness-p13-agent-middl…
AlemTuzlak Sep 28, 2026
ff6844a
feat(ai-mcp): serve a harness as an MCP server
AlemTuzlak Sep 28, 2026
c341668
feat(ai-harness-cli): add --mcp and --yes, and serve MCP at /mcp
AlemTuzlak Sep 28, 2026
d66990e
docs(harness): use a harness from any MCP client
AlemTuzlak Sep 28, 2026
e6f3fa2
test(ai-mcp): cover the connector branches after the v2 client move
AlemTuzlak Sep 28, 2026
d28914b
fix(ai-mcp): concrete .d.ts import path, and cover the harness MCP se…
AlemTuzlak Sep 28, 2026
5945199
fix(ai-mcp): answer each interrupt kind the right way in the harness …
AlemTuzlak Sep 28, 2026
905d9f7
Merge the P7 connector tests into the combined branch
AlemTuzlak Sep 28, 2026
d7da6b9
feat(ai-harness): add the AG-UI bridge subpath
jherr Sep 26, 2026
c82a5ad
fix(ai-harness): coalesce multi-turn operations for strict AG-UI clients
jherr Sep 26, 2026
de730ec
feat(examples/agent-dashboard): live session view + approval queue
jherr Sep 26, 2026
b6f39e7
feat(examples/agent-dashboard): control plane β€” history, spend, config
jherr Sep 26, 2026
74e4597
feat(examples/agent-dashboard): meta-chat over live agent state
jherr Sep 26, 2026
ab9923a
docs: add STATUS.md summarizing the agent-dashboard build
jherr Sep 26, 2026
be6e9d0
feat(examples/agent-dashboard): teams reframe (Phase 1) + Alem protoc…
jherr Sep 26, 2026
0041255
docs: update STATUS.md with the teams reframe (Phase 1)
jherr Sep 26, 2026
d410e78
feat(ai-harness): out-of-band tool invocation + tool visibility
jherr Sep 27, 2026
2078d29
feat(examples/agent-dashboard): Phase 2 β€” tool registry + injection
jherr Sep 27, 2026
cb726c5
docs: update STATUS.md with teams Phase 2 (tool registry + injection)
jherr Sep 27, 2026
0eda69e
feat(ai-harness): systemPreamble on the prompt op + in-band tool thre…
jherr Sep 27, 2026
fcc699b
feat(examples/agent-dashboard): teams Phase 3 β€” system tools, channel…
jherr Sep 27, 2026
089617a
docs: update STATUS.md with teams Phase 3 (system tools, channels, po…
jherr Sep 27, 2026
7cbb240
feat(examples/agent-dashboard): persist agent + team state on the server
jherr Sep 27, 2026
7b35c79
docs: update STATUS.md with server-side persistence
jherr Sep 27, 2026
51cf847
fix(examples/agent-dashboard): resolved approval no longer resurfaces…
jherr Sep 27, 2026
e8d40f3
feat(examples/agent-dashboard): move demo controls into a devtools panel
jherr Sep 27, 2026
7956eaa
docs: update STATUS.md with demo controls devtools panel
jherr Sep 27, 2026
1036f82
feat(examples/agent-dashboard): Reddit pod β€” real service + real LLM
jherr Sep 27, 2026
993e28c
docs: update STATUS.md with the Reddit pod (real service + real LLM)
jherr Sep 27, 2026
9b0fd53
feat(examples/agent-dashboard): product team-composition UI + default…
jherr Sep 27, 2026
8262872
docs: update STATUS.md with the product team-composition UI
jherr Sep 27, 2026
830c514
fix(examples/agent-dashboard): DM membership + surface pod memory on …
jherr Sep 27, 2026
b8e5381
docs: update STATUS.md with the DM membership + team-page memory fix
jherr Sep 27, 2026
69ba301
feat(examples/agent-dashboard): collapsible JSON trees + markdown mes…
jherr Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/harness-ag-ui-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@tanstack/ai-harness': minor
---

New subpath `@tanstack/ai-harness/ag-ui`: the AG-UI bridge. A harness session already streams AG-UI events, so this is a thin, documented seam for AG-UI clients.

- `sessionEventsToAgUi(events, options?)` normalizes a session's `SessionEvent` stream into a pure AG-UI event stream: run usage is surfaced under `metadata.tanstack.usage` (`normalizeUsage` handles both the AG-UI spec array and the TanStack prompt/completion shapes), interrupt/approval waits stay as `RUN_FINISHED` with `outcome.type === 'interrupt'`, and subagent attribution is preserved. It can optionally drop the harness-native `CUSTOM` control events or emit interim `tanstack.spend` `CUSTOM` ticks for a live spend meter.
- `createAgUiHandler(options)` is a `fetch` handler a bare `@ag-ui/client` `HttpAgent` can point at. `POST` a `RunAgentInput` to run a prompt, or one with `resume` entries to answer the last turn's interrupts. It streams AG-UI events as SSE via `@ag-ui/encoder`'s `EventEncoder` (protobuf framing when the client's `Accept` prefers it). The stream is strict AG-UI by default (harness control events dropped so it begins with `RUN_STARTED`); the harness-native approval path and the relay dashboard are unchanged.
- `operationToAgUiRun(entries, options)` coalesces one harness operation β€” which may span several model turns (each a `RUN_STARTED`/`RUN_FINISHED` pair) β€” into a single valid AG-UI run: one `RUN_STARTED` (synthesized when a resumed run leads with a tool result), one terminal `RUN_FINISHED` carrying the operation outcome and summed usage, and `RUN_FINISHED.usage` conformed to the AG-UI `SpecTokenUsage[]` shape. This is what makes the stream consumable by a strict `@ag-ui/client` without "run already finished" / usage-shape errors. `createAgUiHandler` uses it per request.

The AG-UI wire is pinned: built and tested against `@ag-ui/core@1.0.0`, `@ag-ui/encoder@1.0.0`, and `@ag-ui/client@1.0.0`.

Generated with Claude Code.
7 changes: 7 additions & 0 deletions .changeset/harness-p10-goal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/ai-harness': minor
---

Add the `goal({ judge })` plugin to `@tanstack/ai-harness/plugins`. `/goal <text>` sets a goal and starts a turn. After each turn, the `judge` model reads the goal and the end of the transcript and decides if the goal is met. If it is not met, the plugin starts the next turn, up to `maxRounds` turns (20 by default). The loop also stops when a turn waits for approval or fails, and when the user sends a message. `/goal` shows the status, `/goal stop` ends the goal, and `/goal resume` continues it. The goal is plugin state, so it survives a restart. The `GoalMet` event tells other plugins and clients that the goal is met.

A plugin's `ctx.session.prompt(text)` now returns the turn, so the plugin can cancel it before it starts. The turn always waits in the queue, also on a harness with `busy: 'reject'`.
7 changes: 7 additions & 0 deletions .changeset/harness-p11-session-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/ai-harness': minor
---

Add `createSessionView(session | client)` at `@tanstack/ai-harness/view`. It keeps a live TanStack Store of everything a UI shows: messages with streaming text, tool calls, and child agents, plus approvals, questions, sign-ins, status, background agents, commands, settings, tools, and plugin state. It has actions (`send`, `command`, `setConfig`, `cancel`, `approve`, `reject`) and typed events (`view.on('approval', ...)`, `view.on(GoalMet, ...)`). It works with any UI library through the TanStack Store adapters or `store.subscribe`, and it works in the browser with `createHarnessClient`.

New reads for it: `session.transcript()`, `session.describe()`, `snapshot().plugins`, and on the client `transcript()`, `describe()`, `answer()`, `command()`, `setConfig()`, and `events({ onConnection })`. `createHarnessHandler` serves `GET .../transcript` and `GET .../describe`. `selectGoal(state)` reads the goal from a view.
5 changes: 5 additions & 0 deletions .changeset/harness-p12-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-harness-cli': minor
---

The CLI has no UI library now: it does not depend on `ink` or `react`. An interactive terminal uses line mode, which now prints from a session view and opens sign-in links in the browser. `runCli(harness, { ui })` runs your own screen with any TUI library: `ui` gets a ready `createSessionView` view and resolves when the user quits. An Ink screen is in `examples/harness-cli/src/tui.tsx`.
8 changes: 8 additions & 0 deletions .changeset/harness-p13-agent-middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tanstack/ai': minor
'@tanstack/ai-harness': minor
---

`@tanstack/ai`: the chat middleware context has `subagentName`, the agent name of a child that runs through `ctx.chat`, and `parentSubagentRunId`, the id of the child that started a nested child. `chat()` takes both as options. A child that calls `ctx.chat({ subagents })` now gives its own children the host middleware (`subagents.binding.chatMiddleware` and `generationMiddleware`) before their own, also when the binding has no budget.

`@tanstack/ai-harness`: plugins can contribute `agentMiddleware`, chat middleware for every agent run: subagents the lead model calls (coding agents too), background agents, and their children. It does not run in the lead turn. Put the same middleware in `middleware` and `agentMiddleware` to see every model call. Run plugins' `generationMiddleware` now also reaches the agents of their turn. The first-party `usage()` plugin now counts the model calls of every agent, not only the lead turn.
8 changes: 8 additions & 0 deletions .changeset/harness-p14-mcp-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tanstack/ai-mcp': minor
'@tanstack/ai-harness-cli': minor
---

`@tanstack/ai-mcp`: add `createHarnessMcpServer` at `@tanstack/ai-mcp/harness`. It serves a harness as an MCP server, so Claude Code, Claude Desktop, Cursor, or another agent can use it. The tools are `chat`, `steer`, `cancel`, `approve`, `reject`, `resolve`, `answer`, and `status`, plus `agent_<name>` for each exposed agent and `command_<name>` for each plugin command. Every tool takes an optional `threadId`. With `approvals: 'ask'` (the default), the client asks the user about each approval when it supports elicitation. Otherwise the approvals come back in the result. `approvals: 'auto'` approves every tool call. Each interrupt in a result has a `kind`: `approval`, `client-tool`, or `generic`. `resolve` answers every kind in one call: `approved` for an approval, and `payload` for the others.

`@tanstack/ai-harness-cli`: `--mcp` serves the harness as an MCP server over stdio, and `--yes` approves every tool call in MCP mode. `--serve` also serves MCP at `/mcp`, behind the same bearer token. Both need `@tanstack/ai-mcp`, which is an optional peer dependency.
13 changes: 13 additions & 0 deletions .changeset/harness-p9-coding-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@tanstack/ai-sandbox': minor
'@tanstack/ai-harness': minor
'@tanstack/ai-harness-cli': minor
'@tanstack/ai-acp': minor
'@tanstack/ai-dashboard': minor
---

`@tanstack/ai-sandbox/harness` adds `codingAgents({ sandbox, agents, workspace? })`: a harness plugin that gives the lead model one tool per coding agent (Claude Code, Codex, Grok Build, or any ACP agent). Each agent runs in the sandbox, keeps its own session per thread (also after a restart), and starts read-only in the harness `plan` mode. `workspace: 'shared'` (default) runs one agent at a time in one sandbox per thread. `'per-agent'` gives each agent its own sandbox. `/fresh [agent]` starts new sessions.

`@tanstack/ai-harness` plugins can contribute `subagents`: agents the model can call as tools.

Child agent work is now visible: the CLI shows each child's tool calls and a finish line with the start of its answer, ACP editors get the child's tool calls, and the dashboard shows a block per child.
17 changes: 17 additions & 0 deletions .changeset/harness-system-preamble.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@tanstack/ai-harness': minor
---

Add an additive `systemPreamble` to the `prompt` input op. When present, its
strings are prepended (ahead of the harness's own `systemPrompts`) as
system/developer messages for that one run β€” a place for a trigger to attach
per-run context (e.g. operational memory) without the agent author doing
anything.

Also make the server-tool execution `context` always carry the live `threadId`
and `runId` (merged over the harness's static `context`) so a tool invoked
in-band (from a model turn) can resolve the calling thread, matching the flat
`{ threadId, runId, signal }` already passed to out-of-band `{ op: 'tool' }`
invocations.

Generated with Claude Code.
12 changes: 12 additions & 0 deletions .changeset/harness-tool-op.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@tanstack/ai-harness': minor
---

Out-of-band tool invocation: a new `{ op: 'tool', name, args?, meta? }` session input runs one registered tool with no model turn. This is the provisional harness surface the agent-dashboard "injection" work builds on (schedules, run-now, webhooks), following the `@tanstack/ai-harness/ag-ui` precedent of shipping dashboard-facing seams here.

- `session.tool(name, args?, meta?)` (and the `{ op: 'tool' }` client input via `applyInput`) resolves the tool from the harness + session-plugin tools, validates `args` against its input schema, and invokes its server executor directly β€” modeled on the existing `command` op. The tool's lifecycle is published into the session feed as a normal AG-UI run (`RUN_STARTED`, `TOOL_CALL_START`/`ARGS`/`END`, `TOOL_CALL_RESULT`, `RUN_FINISHED`), so watchers render and persist it exactly like a tool call the model made. `meta` (e.g. an injection trigger) is echoed onto the run as a `tanstack.injection` `CUSTOM` event. Injected tools are fire-and-forget (no approval interrupt, like commands).
- Tool **visibility**: a new `toolVisibility?: Record<string, 'public' | 'private'>` on `defineHarness`. Tools default to `private` β€” only tools named `public` may be invoked out-of-band; unknown or private tools are rejected (`unknown_tool` / `not_public`). Visibility is reported per tool in the capabilities document (`capabilitiesOf().tools.items[].visibility`).

Additive and backward compatible: existing ops, tools, and streams are unchanged.

Generated with Claude Code.
151 changes: 151 additions & 0 deletions PODS-PROTOCOL-PROPOSAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Pods/Teams β€” harness protocol proposal (for @AlemTuzlak)

**Status:** proposal Β· **Date:** 2026-09-26 Β· **Author:** Jack + Claude Code
**Basis:** `~/Downloads/pods-design-doc.md` (v2) and `pods-implementation-plan.md`

This is a **discussion doc, not a change**. No `packages/ai-harness` or
`packages/ai-dashboard` (relay) code has been touched. Phase 1 of the teams
reframe ships entirely in `examples/agent-dashboard` (dashboard-local
collections). Everything below is the net-new **harness/relay protocol surface**
that Phases 2+ need, grounded in what the current code does and doesn't do, so we
can agree on the shape before writing any of it.

The design doc says "pod"; the dashboard ships the noun **"team"**. Same concept.

## Why this doc exists

Three capabilities the design depends on are not expressible in the harness as it
stands today. Each is core protocol surface, so it needs your review before
implementation. Findings were verified against the current tree (head at the top
of the harness PR stack).

---

## 1. Out-of-band tool invocation (`injectToolCall`)

**Design need.** The dashboard's injection model (design Β§5.4) calls a single
named tool deterministically, with **no model turn and zero tokens**
(`injectToolCall(pod, tool, args)`), and posts the result to the stream. This is
the *default* trigger path (timers, webhooks, "run now").

**Current reality.** Every tool call originates from a model `chat()` turn. The
input ops are fixed:

```
INPUT_OPS = ['prompt','steer','followUp','resolve','agent','cancel','command','answer','config']
// packages/ai-harness/src/protocol.ts:37
```

There is no op that runs a registered tool directly. The closest precedent is
plugin **commands** (`session.command(name, input)`, `session.ts:579`), which run
user-initiated actions outside a model turn and return a `Receipt` β€” this is the
shape to copy.

**Proposal.**
- Add input op `{ op: 'tool', name: string, args: unknown }` to `INPUT_OPS`
(`protocol.ts:37`) and a case in `applyInput()` (`protocol.ts:119`) that
resolves the tool, validates `args` against its schema, invokes
`tool.execute(args)` **without** opening a `chat()` stream, and returns a
`Receipt` (mirror `executeCommand`).
- Publish the result into the feed under a fresh `operationId` so it renders in
the stream like any tool result (reuse `OperationImpl`/`SessionFeed`).

**Open questions for you.** Should this reuse the command machinery outright
(register injectable tools *as* commands) rather than a parallel op? What runs the
tool's `needsApproval` gate on the injection path β€” does an injected tool that
needs approval still raise an interrupt?

## 2. Tool registry + visibility (`public` vs `private`)

**Design need.** A pod tool registry the dashboard can query, with visibility:
`public` tools are pod-invocable (dashboard, other agents, schedules); `private`
tools run only in the owning agent's own runs (design Β§5.2).

**Current reality.** Tools are static per harness (`HarnessConfig.tools`,
`define.ts:36`), collected per-run from harness + plugins (`session.ts:1080`).
`expose` exists but only for **agents**, not tools:

```
expose?: { agents?: ReadonlyArray<...> } // define.ts:58
```

`capabilitiesOf()` lists harness-level tool names only (`protocol.ts:197`), with
no visibility concept and no plugin/discovered tools.

**Proposal.**
- Add `expose?: { tools?: ReadonlyArray<string> }` to `HarnessConfig`
(`define.ts`), defaulting to private (owner-only).
- Add `session.tools()` mirroring `session.commands()` (`session.ts:562`).
- Have `capabilitiesOf()` (`protocol.ts:187`) return the visibility-filtered set,
so the dashboard registry query and the Β§1 injection path share one source of
truth.

## 3. Injection to offline hosts vs. the relay constraint

**Design need.** "Injected tool calls execute on the owning agent's host; offline
hosts use the relay's existing queued-input behavior rather than silent drops"
(design Β§5.4).

**Current reality β€” this is a real conflict, flagging it explicitly.** The
offline queue exists, but it is **private** to the relay:

```
const sendToHost = (host, envelope): 'sent' | 'queued' => {
if (host.streams.size === 0) { host.queue.push({ envelope, expiresAt: ... }); return 'queued' }
...
} // packages/ai-dashboard/src/server.ts:160
```

`sendToHost()` is called only from `/api/sessions/:host/:thread/input` and
`/open`. There is **no external API to enqueue** for an offline host, and the
queue is drained only on the host's `/api/host/stream` reconnect
(`server.ts:253`). So:

- Adding an injection route that reuses the queue **requires modifying the relay**
(add a route that calls `sendToHost()`), which collides with the hard "don't
modify the relay" constraint.
- Duplicating the queue outside the relay **does not work**: the relay flushes
only its own queue on reconnect, so externally-queued frames would never be
delivered.

**Proposal / decision needed from you.** Pick one:
- **(a)** Accept a minimal, additive relay change: one new frame type
(`harness.inject`) enqueued through the existing `sendToHost()` path. Smallest
possible surface; keeps offline semantics correct. (Recommended β€” the
constraint exists to protect the relay's protocol, and this is protocol we'd be
co-designing with you rather than a unilateral edit.)
- **(b)** Keep injection **online-only** for now (dashboard injects only to hosts
with a live stream; offline injection is deferred). No relay change; weaker
guarantee.

## 4. Per-event causal metadata (loop-TTL bounce protection)

**Design need.** Bounce protection (design Β§6.3) leads with a **causal depth
TTL**: every event carries its causal chain and the pod caps reaction depth. That
requires per-event provenance.

**Current reality.** Only operation-level lineage exists β€” `parentRunId` on turns
for agent resume chains (`session.ts:163`), passed to `chat({ parentRunId })`.
`SessionEvent` itself (`feed.ts`) carries `{ cursor, operationId, event }` β€” no
`parentEventId`, no `causedByInputId`, no `depth`.

**Proposal.**
- Extend the feed/`SessionEvent` with `parentEventId?`, `causedByInputId?`, and a
monotonically-increasing `depth` set when an operation is spawned in reaction to
another event.
- Enforcement (TTL cap, cycle detection, quarantine) stays in the
dashboard/harness layer per the design; this proposal is only about **carrying**
the provenance so enforcement is possible later.

**Open question.** Is `parentRunId` enough to derive depth for the agent-to-agent
case, or do we genuinely need event-level parentage (I believe we do, because a
single run reacts to many upstream events)?

---

## Phasing implication

Phases 2–7 of the implementation plan are all gated on Β§Β§1–4. Phase 1 (the
dashboard-local teams reframe) is done and needs none of this. Recommend a short
review pass on Β§Β§1–3 first (they unblock the injection + registry work in Phase
2), with Β§4 reviewed alongside Phase 4 (bounce protection).
Loading
Loading