Skip to content

AI sessions: adopt pi-coding-agent SessionManager end-to-end#3360

Open
youknowriad wants to merge 14 commits intotrunkfrom
claude/great-nightingale-feb947
Open

AI sessions: adopt pi-coding-agent SessionManager end-to-end#3360
youknowriad wants to merge 14 commits intotrunkfrom
claude/great-nightingale-feb947

Conversation

@youknowriad
Copy link
Copy Markdown
Contributor

@youknowriad youknowriad commented May 6, 2026

Related issues

How AI was used in this PR

Refactor pair-programmed with Claude Code. Reviewed end to end before opening for review.

Proposed Changes

The pi-runtime adoption (#3337) left two layers of legacy plumbing inherited from the pre-pi runtime:

  1. The runtime synthesized Claude-Agent-SDK-shaped SDKMessage events from pi's native AgentEvents (~250 lines of make* builders) so existing consumers could keep parsing the SDK shape.
  2. A sidecar *.openai-state.json mirrored the agent's in-memory transcript next to the recorder's JSONL, because the JSONL itself stored synthetic-SDK events instead of native pi messages.

This PR cuts both: the runtime emits a small AgentRuntimeEvent union (pi events plus compaction_start / compaction_end / turn_completed — shapes match pi-coding-agent's AgentSessionEvent so a future AgentSession swap is drop-in), and the on-disk JSONL is now pi-coding-agent's SessionEntry-based format end to end. Studio metadata (site selection, agent questions, tool progress, etc.) rides as studio.* CustomEntry payloads in the same file.

Migration: a one-shot eager sweep at app launch (apps/studio/src/index.ts app.on('ready') and apps/cli/commands/ai/index.ts runCommand entry) walks ~/.studio/sessions/** and rewrites any pre-pi session.started v1 JSONL into pi format in place. Idempotent — pi-format files are skipped, so subsequent boots are no-ops. A lazy fallback in openStudioSession catches any file the eager sweep missed.

Consumers rewritten natively (no translator at any layer):

  • tools/common/ai/sessions/{summary,filter-events,active-site,store}.ts and models.ts.resolveSessionModel consume studio.* custom entries and model_change entries directly.
  • apps/cli/ai/sessions/{replay,context}.ts iterate SessionManager.getEntries().
  • apps/cli/ai/runtimes/pi/index.ts persists via SessionManager.appendMessage — no per-turn legacy-event encoder.
  • apps/cli/commands/ai/index.ts writes Studio metadata via appendStudioEntry<T>(sm, customType, data).
  • apps/studio IPC writes model_change for UI-driven /model swaps via appendModelChangeEntry (pi's native discriminator, not a custom entry) so resume-context resolution and pi's own session tooling both pick the override up.
  • apps/ui/src/components/session-view/{conversation,composer,index}.tsx and apps/ui/src/data/queries/use-agent-run.ts consume pi entries; optimistic mutations produce pi entries with synthetic ids that the next run.exited refetch replaces with disk-backed reals.

Cache invalidation: LoadedAiSession.events: AiSessionEvent[] becomes entries: SessionEntryBase[]. The renderer's @tanstack/react-query-persist-client buster key is bumped so persisted localStorage caches from prior builds are dropped on first load after deploy.

Deletions:

  • apps/cli/ai/runtimes/messages.ts (synthetic SDKMessage type)
  • apps/cli/ai/runtimes/pi/persistence.ts (sidecar)
  • apps/cli/ai/sessions/recorder.ts
  • tools/common/ai/sdk-messages.ts
  • The mcp__studio__ prefix-stripping shim in tools/common/ai/tools.ts
  • Three legacy-paths test files

What this unlocks (out of scope for this PR):

  • AgentSession adoption from pi-coding-agent. The runtime event shapes already match AgentSessionEvent and disk format is pi-managed, so the swap drops apps/cli/ai/runtimes/pi/compaction.ts (~130 lines) and gives us pi's auto-retry / branching / bash-execution queue for free. Estimated ~150 lines changed when we do it.
  • Compaction summaries and model_change history now persist on disk via pi's session machinery (the legacy format couldn't represent them).

Testing Instructions

  • Resume an existing session created before this PR (legacy JSONL on disk). The eager migration should rewrite the file in place on first launch and the conversation should replay correctly. Inspect ~/.studio/sessions/<YYYY>/<MM>/<DD>/*.jsonl — the first line should change from {"type":"session.started","version":1,...} to {"type":"session","version":3,...} after first open.
  • Start a new session, exchange a few turns, kill the process, then resume. The transcript should hydrate from disk via pi's SessionManager.buildSessionContext() — there's no *.openai-state.json sidecar anymore.
  • Open the desktop app's session sidebar with a mix of new (pi-format) and migrated sessions. Both should show populated firstPrompt, ownerSitePath, activeEnvironment, etc. (the symptom of the read-side asymmetry that earlier iterations of this PR had — fixed because every reader now consumes pi entries natively).
  • Run studio code --json "hello". Wire format keeps the legacy 'message' envelope but the inner payload is the native AgentRuntimeEvent.
  • Trigger compaction (long enough conversation to cross the threshold). The "Compacting conversation history…" loader and "Conversation history compacted" info line should still fire.
  • Trigger an AI usage cap on the wpcom provider. The cap message should still appear — detection moved from scanning text blocks to checking pi's AssistantMessage.errorMessage for a 429 marker.
  • Open a session that had no turns yet (just a header on disk). The conversation view should render empty without crashing — confirms the data.entries ?? [] guard.

Pre-merge Checklist

  • TypeScript / React / console errors checked (typecheck clean across all 4 workspaces)
  • 1534 vitest tests passing, 0 failures
  • Manual smoke pass on the test plan above
  • Code review

🤖 Generated with Claude Code

youknowriad and others added 2 commits May 6, 2026 13:44
The pi runtime adoption (#3337) left behind a translation layer that
synthesized Claude-Agent-SDK-shaped `SDKMessage` events from pi's native
`AgentEvent`s, plus a sidecar JSON file that mirrored the agent's
in-memory transcript next to the recorder's JSONL. Both were inherited
from the pre-pi runtime — pi already gives us typed `AgentMessage`s and
ships a full `SessionManager` (versioned JSONL, append-only tree,
built-in v1→v3 migration, custom-entry hook).

This collapses the indirection on the CLI side:

* Runtime emits a small `AgentRuntimeEvent` union (pi's `AgentEvent` plus
  `run_started` / `compaction` / `turn_completed` / `runtime_error`).
* Conversation transcript lives in pi's `SessionManager` — no separate
  sidecar. The runtime appends user/assistant/tool messages directly.
* Studio metadata (site selection, paused turns, progress messages,
  agent questions, etc.) rides as typed `studio.*` `CustomEntry` payloads
  in the same JSONL.
* Legacy `session.started v1` JSONL files migrate in place on load
  (`legacy-migration.ts`), so resumed sessions keep working.

Net deletion: ~250 lines of `make*` builders in the runtime, the
sidecar persistence module, the recorder class, the SDKMessage type,
and three test files that were testing those legacy paths.

Phase 2 (renderer + apps/studio + sdk-messages.ts shim) is a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Followups to the SessionManager adoption — trim the wrappers around pi
to keep the surface as close to native pi as possible.

* Shrink `tools/common/ai/sessions/entry-types.ts` from a full mirror of
  pi's session types (~280 lines) to just Studio's own custom-entry
  shapes plus a structural `SessionEntryBase` for renderer consumption.
  CLI code imports `SessionEntry` and `AgentMessage` directly from pi.
* Collapse seven named `appendSiteSelected` / `appendToolProgress` /
  etc. helpers into a single typed `appendStudioEntry<T>(sm, type, data)`
  that's still discriminated through `StudioCustomEntryDataMap`.
* Replace `run_started` + `runtime_error` events with `turn_completed`
  carrying the error in `result` — one fewer event type in the union and
  consumers don't grow new branches.
* Rename our `compaction { phase }` event to match pi-coding-agent's
  `AgentSessionEvent.compaction_start` / `compaction_end` shape so the
  eventual `AgentSession` adoption is a drop-in.
* Drop the `lastResultSessionId` bookkeeping in commands/ai/index.ts —
  pi's SessionManager hydrates the next turn from disk, so the retry
  prompt is always "Continue from where you left off."

Net deletion: ~220 lines, no behavior change. Typecheck clean across
all 4 workspaces, 1487 tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
youknowriad and others added 5 commits May 6, 2026 14:22
Phase 1 left the storage layer asymmetric: the CLI runtime wrote pi-format
JSONL but `tools/common/ai/sessions/store.ts` (used by apps/studio's IPC
handlers and the renderer's sidebar) still parsed every line as a legacy
`AiSessionEvent` and bailed when it didn't match — so any pi-format file
(brand-new sessions, or legacy ones touched by the CLI's resume path)
listed with empty `firstPrompt` / `ownerSitePath` / `eventCount`. The
sidebar looked broken.

This puts the migrator and a pi → legacy translator in tools/common so
the read path is symmetric again:

* `migration.ts` — the legacy → pi-format rewriter, parameterized on cwd
  (was hard-coded to `STUDIO_SITES_ROOT` in apps/cli). Uses structural
  shapes for pi entries so tools/common stays free of pi runtime imports.
* `pi-translation.ts` — `piEntriesToLegacyEvents()` (used by the read
  path) and `legacyEventToPiEntries()` (used by `appendAiSessionEvent`
  when the on-disk file is pi format, e.g. for `setSessionEnvironment`
  and `setAiSessionModel` writes from the desktop IPC layer).
* `store.ts` — `readAiSessionEventsFromFile` now migrates the file in
  place (no-op if already pi) then translates entries back to the legacy
  view downstream code expects. `createAiSession` writes pi format
  directly. `appendAiSessionEvent` detects format and dispatches.

Disk truth is now consistently pi format (new sessions, migrated legacy
sessions, and CLI runtime sessions all write the same shape). The
`AiSessionEvent[]` type stays as the in-memory abstraction every reader
consumes — the renderer doesn't need to change.

Drops `LoadedAiSession.entries` (no longer needed; the `entries` field
was a Phase-2 placeholder for direct pi-entry consumption that the
read-path translator made unnecessary). Also reverts
`apps/cli/ai/sessions/context.ts` to read legacy events again (now that
those events flow correctly out of pi-format files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oundary

The Phase 2 storage layer (#3360) moved disk to pi format and added a
pi↔legacy translator on the read/write path in tools/common. That made
the sidebar work for new pi-format sessions, but at the cost of ~810
lines of bidirectional translation + a one-shot migrator. Reviewer
feedback: "this feels complex, can't we just keep things simple?"

Yes. Disk format goes back to the pre-Phase-1 legacy `AiSessionEvent[]`
JSONL — unchanged across the entire history of this PR — and the pi↔
legacy bridge moves into the only place it's inherent: the CLI runtime,
which is the sole consumer of pi `AgentMessage[]`.

* Disk: legacy `AiSessionEvent[]` JSONL, exactly as before.
* In-memory agent state: pi `Agent` (in `AGENTS_BY_SESSION` map keyed
  by sessionId), unchanged from Phase 1.
* Hydration: on cold-start the runtime reads the legacy events from the
  session file and translates `sdk.message` events back into pi
  `AgentMessage[]` for `Agent.initialState.messages`.
* Persistence: on `turn_end` the runtime translates the new pi assistant
  message + tool results into legacy `sdk.message` events and appends
  them via `fs.appendFile`. The orchestrator writes Studio metadata
  events (site selection, agent question, turn closed, etc.) directly
  via `appendAiSessionEvent` — no Studio-custom-entry layer at all.

Net delete: ~700 lines (`migration.ts`, `pi-translation.ts`,
`entry-types.ts`, `studio-entries.ts`, `pi-session.ts`). New: ~140-line
`apps/cli/ai/runtimes/pi/messages.ts` covering both translation
directions.

Sidebar now populates correctly because the disk format never changed —
no migration needed, no translator in `tools/common`. The summary /
filter / active-site / store helpers go back to reading legacy events
verbatim. The renderer (apps/ui) wasn't touched in either Phase 2 pass
and continues to work unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous "legacy disk + runtime translator" attempt left the CLI as
the only place that knew about the pi shape. This goes the other way:
the on-disk JSONL becomes pi-coding-agent's `SessionEntry`-based format
end-to-end, and every consumer (sidebar listing, session view, replay,
resume context, model resolution, optimistic mutation cache) reads pi
entries directly. There is no translator anywhere.

The migration is a one-shot eager sweep at app launch:

* `apps/studio/src/index.ts` `app.on('ready', ...)` calls
  `migrateAllSessions(rootDir, cwd)` — walks `~/.studio/sessions/**`,
  rewrites any legacy `session.started v1` JSONL into pi format in
  place, and reports counts. Pi-format files are skipped, so the second
  boot is a no-op.
* `apps/cli/commands/ai/index.ts` `runCommand()` runs the same sweep on
  CLI entry so `studio code` from a fresh shell also catches anything
  apps/studio missed.
* `apps/cli/ai/sessions/pi-session.ts` keeps a lazy fallback in
  `openStudioSession` so an unmigrated file can still be opened
  individually.

What changed downstream:

* `LoadedAiSession.entries: SessionEntryBase[]` replaces `events:
  AiSessionEvent[]`. The `AiSessionEvent` union and
  `tools/common/ai/sdk-messages.ts` are deleted entirely.
* `tools/common/ai/sessions/{summary,filter-events,active-site}.ts` and
  `models.ts.resolveSessionModel` rewritten to consume pi entries —
  they look at `studio.*` `customType` payloads and `model_change`
  entries instead of legacy event types.
* `tools/common/ai/sessions/store.ts` writes pi format via `createAiSession`
  and exposes `appendStudioEntry<T>` / `appendModelChangeEntry` for the
  desktop IPC layer's `setSessionEnvironment` / `setAiSessionModel`
  handlers (replacing `appendAiSessionEvent`).
* `apps/cli/ai/runtimes/pi/index.ts` goes back to persisting via
  `SessionManager.appendMessage` — no per-turn legacy-event encoder. The
  ~140-line runtime translator (`messages.ts`) is deleted.
* `apps/cli/ai/sessions/{replay,context}.ts` consume pi entries from
  `SessionManager.getEntries()` directly.
* `apps/cli/commands/ai/index.ts` writes Studio metadata as `studio.*`
  custom entries via `appendStudioEntry`.
* `apps/ui/src/components/session-view/{conversation,composer,index}.tsx`
  + `apps/ui/src/data/queries/use-agent-run.ts` all switch to
  `entries: SessionEntryBase[]`, with optimistic mutations producing pi
  entries (synthetic ids; the next refetch on `run.exited` replaces them
  with the disk-backed reals).

What we get back from this:

* Future-compat with pi-coding-agent's session features (compaction
  summaries, branching, model_change history, bash-execution log).
* Phase 3 — adopting `AgentSession` from pi-coding-agent — becomes a
  drop-in: pi already speaks our session shape. Drops `compaction.ts`
  and gives us auto-retry / branching for free.
* `toolName` on pi `ToolResultMessage` is recorded properly (pi requires
  it).

Validated: typecheck clean across all 4 workspaces, 1534 tests passing
(updated `create-session.test.ts` + `environment.test.ts` to assert
pi-format reads + custom-entry writes; `pi-runtime.test.ts` mocks
`SessionManager.inMemory()` via `vi.mock` with `importOriginal()` so the
real export reaches the runtime).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strip the explanatory prose I sprinkled across the new files. Most of
it was restating what the code already said. Net: -167 lines, no
behavior change. Typecheck clean, 1534 tests passing.
The renderer persists query state to localStorage via
@tanstack/query-sync-storage-persister. The previous build cached
LoadedAiSession with `events: AiSessionEvent[]`; after Phase 2 it's
`entries: SessionEntryBase[]`. Stale cache hydration left `prev.entries`
undefined and crashed `entries.length` on empty/cached sessions.

* Bump `buster: '1' → '2'` in apps/ui/src/data/core/query-client.ts so
  every persisted query is dropped on first load after deploy.
* Defensive `data.entries ?? []` in conversation/index.tsx and
  `prev.entries ?? []` in use-agent-run.ts + composer/index.tsx so a
  malformed cache entry from any other source can't crash the same way.
@youknowriad youknowriad marked this pull request as ready for review May 6, 2026 14:39
@youknowriad youknowriad changed the title CLI agent: drop synthetic SDKMessage, persist via pi SessionManager AI sessions: adopt pi-coding-agent SessionManager end-to-end May 6, 2026
Comment thread apps/studio/src/index.ts
Comment thread tools/common/ai/sessions/entry-types.ts
- Drop eager migration sweep on app/cli boot — readPiFileEntries already
  migrates on first read, so the bulk pre-pass was redundant work that
  also blocked startup until every JSONL had been touched.
- Stop re-declaring SessionEntry as a structural mirror; import the
  union from @mariozechner/pi-coding-agent directly.
- Replace the runtime-emitted turn_completed envelope with pi's own
  agent_end. Consumers (CLI UI, JSON adapter, eval runner, pi-runtime
  test) now derive success/error from the last AssistantMessage's
  stopReason and count their own turns from turn_end events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wpmobilebot
Copy link
Copy Markdown
Collaborator

wpmobilebot commented May 6, 2026

📊 Performance Test Results

Comparing d46f5d0 vs trunk

app-size

Metric trunk d46f5d0 Diff Change
App Size (Mac) 1409.57 MB 1409.59 MB +0.02 MB ⚪ 0.0%

site-editor

Metric trunk d46f5d0 Diff Change
load 1523 ms 1497 ms 26 ms ⚪ 0.0%

site-startup

Metric trunk d46f5d0 Diff Change
siteCreation 8096 ms 8081 ms 15 ms ⚪ 0.0%
siteStartup 4943 ms 4932 ms 11 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

youknowriad and others added 2 commits May 6, 2026 17:19
Drops our chars/4 estimator + synthetic-summary-as-user-message approach
in favor of pi-coding-agent's compaction primitives. We now:

- Trigger via pi's accurate token math (`calculateContextTokens` against
  the LLM's reported `usage`) and pi's overflow detector
  (`isContextOverflow` from pi-ai), instead of hand-rolling both.
- Cut at proper turn boundaries via `findCutPoint` over `SessionEntry[]`,
  so tool-result chains and bash entries stay paired with their turn.
- Generate the summary via `generateSummary` (with chained
  `previousSummary` for iterative compactions) and persist via
  `sessionManager.appendCompaction(...)` — the JSONL now records a real
  `CompactionEntry` instead of a synthetic user message.
- Rebuild `agent.state.messages` from `sessionManager.buildSessionContext()`
  after compaction so the next request sees the trimmed transcript.
- Run on `agent_end` (post-turn) plus a pre-flight check before each
  prompt — same dual trigger AgentSession uses. Overflow gets a
  compact-and-continue recovery via `agent.continue()`.
- Emit the `compaction_start` / `compaction_end` shapes from pi's
  `AgentSessionEvent` (now carries `result: CompactionResult | undefined`)
  instead of declaring our own.

System prompt control stays with us — we don't go through `AgentSession`
because that would clobber `agent.state.systemPrompt` on every turn.

Pi 0.70.2 doesn't export `prepareCompaction` or `estimateContextTokens`
(used by AgentSession internally), so we walk session entries ourselves
and skip pi's split-turn merge — when `findCutPoint` lands inside a
turn, we round up to the turn start (one extra turn stays verbatim,
no correctness issue).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The studio-entries module was a 14-line file containing one 1-line
wrapper used only by the CLI orchestrator. Moving it into
commands/ai/index.ts as a local function removes one indirection
without losing the type-safe `(customType, data)` signature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread apps/cli/ai/runtimes/runtime-events.ts Outdated
youknowriad and others added 4 commits May 6, 2026 23:02
Cuts the auto-compaction footprint by ~320 lines.

What's gone:
- Pre-flight compaction before each `agent.prompt()`. A session resumed
  past the threshold now wastes one LLM call before the post-end check
  trims; rare and recoverable.
- `agent.continue()` retry after an overflow compaction. On overflow
  the user sees the error once and re-prompts; the next request ships
  the compacted transcript and succeeds.
- The `overflowRecoveryAttempted` state, cross-model overflow guard,
  stale-pre-compaction-usage guard, and the bespoke
  `CompactionDecision` discriminated union — all edge cases the
  simpler flow doesn't need.

`auto-compaction.ts` is now a thin wrapper: `shouldCompact()` returns
`'threshold' | 'overflow' | null` from pi's helpers, and
`runCompaction()` does findCutPoint → generateSummary →
appendCompaction → buildSessionContext. The pi/index.ts wiring is
~25 lines of compaction-event plumbing tucked into the existing
agent.subscribe handler.

`runtime-events.ts` collapses to a single re-export: `AgentRuntimeEvent
= AgentSessionEvent`. No more hand-derived compaction event types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracted `AiChatUI.renderToolResults(results)` from the live
`turn_end` handler so replay can render tool results directly instead
of constructing a fake assistant + synthetic `turn_end` event just to
reuse the rendering path. Drops ~25 lines of placeholder ceremony in
replay.ts and the redundant `as StudioCustomEntry<...>` casts after
each `isStudioCustomEntryOfType()` guard (the guard already narrows).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`runtime-events.ts` was reduced to a one-line alias (`AgentRuntimeEvent
= AgentSessionEvent`) plus three derived types nobody imported. Delete
the file and have the six consumers (output-adapter, eval-runner, ui,
pi-runtime test, pi runtime, runtime types) import `AgentSessionEvent`
straight from pi-coding-agent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rsistence

Four post-review fixes for the pi-migration PR.

1. /clear regression. `clearSession()` was only appending the
   `studio.session_cleared` marker; the cached `Agent`'s
   `state.messages` and the cold-resume `buildSessionContext()` both
   ignored the marker, so the model still saw pre-clear turns.
   - `pi/index.ts` exports `clearAgentForSession(sessionId)`; the
     orchestrator drops the cache after appending the marker.
     Cold-resume initial messages now flow through
     `filterEntriesAfterLastClear` before `buildSessionContext`.
   - Test in `pi-runtime.test.ts` asserts pre-clear text doesn't
     reach the rebuilt agent.

2. Remote-session reply. `extractResultPayload` in
   `turn-runner.ts` only matched the legacy SDK `result` envelope;
   post-pi the wire carries `agent_end`, so `replyText` was always
   undefined and Telegram fell back to "Local agent did not return a
   result". Rewrote it to walk `agent_end.messages` for the last
   assistant's text + `stopReason`. Updated the test fixture
   (`mock-studio-code.mjs`) to emit pi-shape `agent_end` envelopes.

3. `--no-session-persistence` removal. The pi runtime requires a
   live `SessionManager`, so the flag's only behavior post-PR was
   "throw on every prompt". Dropped the option from
   `apps/cli/index.ts`, `commands/ai/index.ts`, and
   `commands/ai/sessions/resume.ts`; tightened `ensureSession()` to
   return a non-undefined `SessionManager`.

4. Legacy Studio Code chat parser. `studio-code-event-parser.ts`
   only recognised SDK shapes (`assistant`/`stream_event`/`user`/
   `result`), so the chat went silent under the new wire format.
   Migrated `parseSdkMessage` (now `parseSessionMessage`) to handle
   `message_end` (assistant text → APPEND_TEXT, toolCall →
   TOOL_USE_START) and `turn_end` (toolResults → TOOL_RESULT).
   Envelope-level events (`turn.started`, `turn.completed`,
   `progress`, `error`, `question.asked`) unchanged — they still
   come from `JsonAdapter.emit*`. Added
   `studio-code-event-parser.test.ts`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants