Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
73cb678
docs(plans): diagnose pane restore backpressure loop and plan respons…
danshapiro Sep 20, 2026
921c736
docs(plans): define safe responsive terminal restore contracts
danshapiro Sep 20, 2026
a507389
docs(plans): carry the current user request block for the restore imp…
danshapiro Sep 20, 2026
a0f5a4d
docs(plans): require a negotiated hidden-pane lifetime claim in the r…
danshapiro Sep 20, 2026
87e8302
docs(plans): split continuation consumption credit from the parser-ap…
danshapiro Sep 20, 2026
362d2c3
docs(plans): add the surface-coverage resume cursor and attach-exact …
danshapiro Sep 20, 2026
9aa5915
feat(ws): negotiate the pacedTerminalReplayV1 capability
danshapiro Sep 20, 2026
d1acb85
feat(protocol): carry restore bounds fields on negotiated attach.read…
danshapiro Sep 20, 2026
c67de43
feat(ws): paced negotiated terminal replay with credit-gated pages
danshapiro Sep 20, 2026
0b2e733
fix(ws): cancel paced sessions on legacy re-attach; tighten page budg…
danshapiro Sep 20, 2026
10af7d2
feat(client): consume paced replay with write-queue credit
danshapiro Sep 20, 2026
ead9731
feat(client): resume pane restores from coverage checkpoints instead …
danshapiro Sep 20, 2026
23b43d2
fix(client): scope checkpoints to surface instances and calibrate rec…
danshapiro Sep 20, 2026
a205fd0
feat(panes): hidden-pane lifetime claims replace background hydration
danshapiro Sep 20, 2026
57c1771
feat(ws): spill before disconnect with drain-progress liveness
danshapiro Sep 20, 2026
c868c31
test(ws): pin the mixed-version restore compatibility matrix
danshapiro Sep 20, 2026
5f1bcd1
fix(terminal): bound write-queue drain by consumed work, not ambient …
danshapiro Sep 21, 2026
477a389
test(gate): resolve the tsx stub absolutely so gitless cloud images c…
danshapiro Sep 21, 2026
c020bb1
test(e2e): incident-shaped paced-restore convergence spec
danshapiro Sep 21, 2026
58f2875
fix(terminal): keep the checkpoint resume authoritative across reconc…
danshapiro Sep 22, 2026
05b92f3
feat(ws): emit spill observability at eviction time, add per-occurren…
danshapiro Sep 22, 2026
1f7adaa
fix(client): retry failed persist flushes; stabilize the T1 attach-en…
danshapiro Sep 22, 2026
3804cab
docs: correct the negotiated-restore attach-intent list and name the …
danshapiro Sep 22, 2026
865532c
test(ws): give term09 flood walls real-time margins for loaded gates;…
danshapiro Sep 22, 2026
e841f65
test(ws): starvation-proof the catastrophic-close observation window …
danshapiro Sep 22, 2026
a2f17a0
fix(ws): grant paced replay pages only on page-end credits
danshapiro Sep 22, 2026
91bbbb8
fix(ws): complete the paced tail against a live producer via a fixed …
danshapiro Sep 22, 2026
f98f69b
fix(client): repair a negotiated queue_overflow gap on the open conne…
danshapiro Sep 22, 2026
81566b8
fix(client): a clean completed restore resets the recovery streak
danshapiro Sep 22, 2026
bc30693
fix(terminal): page the paced tail handoff and report its retention gaps
danshapiro Sep 22, 2026
e7147b9
fix(client): quarantine unexplained forward sequence jumps as implici…
danshapiro Sep 22, 2026
f667312
fix(client): repair queue_overflow gaps from the checkpoint cursor, n…
danshapiro Sep 22, 2026
ee5561e
fix(ws): complete the paced drain at a fixed target off-dispatch, nev…
danshapiro Sep 22, 2026
40afe39
fix(client): reset the recovery streak only on cursor-confirmed conve…
danshapiro Sep 22, 2026
3e198bb
fix(client): disarm the deferred viewport clear on a declared unrecon…
danshapiro Sep 22, 2026
950d30b
test(e2e): assert the exhaustion cycle preserves the visible content …
danshapiro Sep 22, 2026
b649435
fix(ws): hand the post-target replay remainder through a bounded page…
danshapiro Sep 22, 2026
90e4afb
fix(client): consume the deferred viewport clear only when replacemen…
danshapiro Sep 22, 2026
e941aa5
fix(ws): bound the paced handoff at the head captured once at complet…
danshapiro Sep 22, 2026
c5b941a
fix(client): apply the deferred viewport clear and gap notices atomic…
danshapiro Sep 22, 2026
a1d96db
fix(client): restore the plan:166 recovery-streak reset rule
danshapiro Sep 22, 2026
74da776
fix(ws): reserve drain admission capacity atomically and clamp the pa…
danshapiro Sep 22, 2026
98ad868
fix(client): show the delivery-gap repair notice in chrome, never on …
danshapiro Sep 22, 2026
63d3909
fix(ws): distinguish the paced tail's two gap-exit reasons in diagnos…
danshapiro Sep 22, 2026
79fda37
fix(ws): reject paced replay credits that carry the wrong stream id
danshapiro Sep 22, 2026
c9112c2
fix(ws): wake gated drain admissions when a reservation is released
danshapiro Sep 22, 2026
d87362b
fix(terminal): clamp the output fragment cap to the paced page budget…
danshapiro Sep 22, 2026
04d39af
test(ws): send the real stream id in paced replay credit fixtures
danshapiro Sep 22, 2026
60c0651
feat(ws): honor the negotiated replayPageBytes page-budget bound
danshapiro Sep 22, 2026
679c83a
fix(ws): sequence a paced session's deferred final output before term…
danshapiro Sep 22, 2026
5ef24f1
fix(protocol): accept integer-valued number spellings for replayPageB…
danshapiro Sep 22, 2026
9ac5238
fix(ws): make replayPageBytes an honest bound end to end
danshapiro Sep 22, 2026
08f526f
fix(ws): sequence a paced session's natural exit behind its credited …
danshapiro Sep 22, 2026
cf567f4
refactor(ws): narrow handle_replay_credit to the registry it uses
danshapiro Sep 22, 2026
303b8fd
test(ws): pin the credited natural-exit races with explicit credit ti…
danshapiro Sep 22, 2026
589042b
fix(ws): arm the staged exit before driving; ride terminal.exit on th…
danshapiro Sep 22, 2026
e8a20f0
test(terminal): add the deterministic in-lock natural-exit staging hook
danshapiro Sep 22, 2026
613b2db
test(ws): pin the concurrent-staging exit races with the deterministi…
danshapiro Sep 22, 2026
aeeafe3
fix(ws): make the paced exit-transition decision atomic with exit sta…
danshapiro Sep 22, 2026
17fe008
fix(client): exempt the legacy covered-window resume from the implici…
danshapiro Sep 23, 2026
9710e69
test(ws): drive the eviction close observation by the monitor's captu…
danshapiro Sep 23, 2026
86ae1ee
test(ws): ignore the load-flaky codex fork-rebind TUI test (user-dire…
danshapiro Sep 23, 2026
2171af8
test(ws): size the eviction test's refill threshold for contended-box…
danshapiro Sep 23, 2026
0ca9461
Merge remote-tracking branch 'origin/main' into the-usual/terminal-re…
danshapiro Sep 23, 2026
ae09177
test(ws): tolerate PTY read-chunk splitting in the paced-exit race tests
danshapiro Sep 23, 2026
ce86028
test(ws): stop racing the drain probe answer against the server-side …
danshapiro Sep 23, 2026
fe3c8d8
ci: retrigger pull_request workflows on the current head
danshapiro Sep 23, 2026
28b3331
ci: allow manual dispatch of the Rust Tests workflow
danshapiro Sep 23, 2026
cc362b4
ci: make the rust-gate changes filter dispatch-compatible
danshapiro Sep 23, 2026
920f0c4
test(freshagent): wait for the materialize spawn's ownership commit b…
danshapiro Sep 23, 2026
da03733
Merge remote-tracking branch 'origin/main' into the-usual/terminal-re…
danshapiro Sep 23, 2026
2e9545c
test(server): give the native retry-floor tests a starvation-robust f…
danshapiro Sep 23, 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
25 changes: 18 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,28 @@ PORT=3001
# Maximum inbound WebSocket frame size in bytes (default: 16777216 = 16MB)
# WS_MAX_PAYLOAD_BYTES=16777216

# Terminal stream replay ring bytes per terminal (default: 262144 = 256KB)
# TERMINAL_REPLAY_RING_MAX_BYTES=262144

# Terminal stream output queue bytes per attached client (default: 131072 = 128KB)
# TERMINAL_CLIENT_QUEUE_MAX_BYTES=131072
# Terminal stream replay ring bytes per terminal (default: 1048576 = 1MB).
# This is the per-terminal retained-output ring every attach replays from:
# negotiated (pacedTerminalReplayV1) clients page it out bounded by
# credits, and its front bound is what `terminal.attach.ready` reports as
# `oldestRetainedSeq` (older output is gone; legacy inline attaches just
# get the retained tail).
# TERMINAL_REPLAY_RING_MAX_BYTES=1048576

# Terminal stream output queue bytes per attached client (default: 16777216 = 16MB).
# This is the SPILL bound: past it, oldest output is evicted with a
# generation-scoped gap instead of disconnecting the client.
# TERMINAL_CLIENT_QUEUE_MAX_BYTES=16777216

# Terminal stream send batch bytes per flush (default: 65536 = 64KB)
# TERMINAL_STREAM_BATCH_MAX_BYTES=65536

# Catastrophic WS bufferedAmount threshold for terminal streaming (default: 16777216 = 16MB)
# TERMINAL_WS_CATASTROPHIC_BUFFERED_BYTES=16777216
# Catastrophic WS bufferedAmount disconnect threshold (default: 67108864 = 64MB).
# Must be STRICTLY greater than TERMINAL_CLIENT_QUEUE_MAX_BYTES — the server
# refuses to start otherwise, so normal pressure spills before any disconnect.
# The close additionally requires zero successful socket sends for the whole
# stall window (a slow-but-progressing client is not a dead socket).
# TERMINAL_WS_CATASTROPHIC_BUFFERED_BYTES=67108864

# Catastrophic stall duration before 4008 close, ms (default: 10000)
# TERMINAL_WS_CATASTROPHIC_STALL_MS=10000
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Rust Tests

on:
workflow_dispatch:
pull_request:

permissions:
Expand Down Expand Up @@ -35,7 +36,12 @@ jobs:
# changed is not proof of fixture compatibility. Scoped to the
# root and workspace members — the independent demo locks are
# not part of the workspace dependency tree.
if git diff --name-only "origin/${{ github.base_ref }}...${{ github.sha }}" \
# `github.base_ref` exists only for pull_request events; manual
# dispatch (the event-routing escape hatch) compares against the
# default branch instead.
BASE="${{ github.base_ref }}"
[ -n "$BASE" ] || BASE="main"
if git diff --name-only "origin/$BASE...${{ github.sha }}" \
| grep -Eq '\.rs$|(^|/)Cargo\.(toml|lock)$|^rust-toolchain|^\.cargo/|^test/fixtures/|^(pnpm-lock\.yaml|pnpm-workspace\.yaml|package(-lock)?\.json|\.npmrc)$|^(crates|packages)/.*package(-lock)?\.json$|^packages/'; then
echo "rust=true" >> "$GITHUB_OUTPUT"
else
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ stderr note). Provisioning, rotation, and revocation live in

**WebSocket Protocol:** Schema-validated messages using Zod. Handshake flow: client sends `hello` with token → server validates → sends `ready`. Message types include `terminal.create/input/resize/detach/attach` and broadcasts like `sessions.updated`. The `ready` frame carries an optional additive `buildId` (the server's artifact-time-baked git commit, `"unknown"` fallback): the client bakes its own at Vite build time (`__FRESHELL_BUILD_ID__`) and, on a mismatch, reloads exactly once per tab session (sessionStorage sentinel `freshell.server-build-reload` records the last attempted server build id; the same id never reloads twice, a different (corrected) deployment re-arms the guard), self-healing stale-client contract errors; `"unknown"` on either side never triggers or clears the guard (`src/lib/server-build-check.ts`). The once-guard is per server identity: an origin fronted by mixed-build servers could oscillate, and a newer client against an older server costs one futile bounded reload per fresh tab session (both accepted for the single-server self-hosted model).

**PTY Lifecycle:** Each terminal has a unique ID. Server maintains 64KB scrollback buffer. On attach, client receives buffer snapshot then streams new output. On detach, process continues running (background session). Configurable idle timeout (15 mins default).
**PTY Lifecycle:** Each terminal has a unique ID. The server retains output in a per-terminal replay ring (default 1 MiB, `TERMINAL_REPLAY_RING_MAX_BYTES`; older output is gone). On attach, a non-negotiated (legacy) client receives the retained tail inline as a snapshot then streams new output; on detach, the process continues running (background session). Configurable idle timeout (15 mins default).

**Negotiated Restore (pacedTerminalReplayV1):** A client whose `hello` negotiated `pacedTerminalReplayV1` restores via a bounded paced-replay session instead of the inline snapshot: `terminal.attach.ready` carries the window bounds (plus `oldestRetainedSeq`), a reconnect resumes from the client's coverage cursor (`requestedSinceSeq`/`effectiveSinceSeq`) when its checkpoint is valid, the server pushes one bounded first page, and further pages flow only on `terminal.replay.credit` continuation credits (stale generations and non-negotiated credits are inert). Retention loss mid-restore is reported as an exact `terminal.output.gap` (bounds-carrying), never as a silent truncation or a disconnect. Attach intents pick their own replay shape (viewport hydrate, transport reconnect, hidden-pane `keepalive_delta`; the load-more-history UI flow rides viewport hydrate today), and hidden panes can claim terminals (`terminalLifetimeClaimV1`) so they survive reaping while invisible. Capability negotiation gates all of this: older clients keep the exact legacy wire behavior, and no healthy process is killed or replaced because replay is missing. Per-connection output pressure spills before it disconnects: past 16 MiB of queued output (`TERMINAL_CLIENT_QUEUE_MAX_BYTES`) the oldest frames are evicted with a generation-scoped `terminal.output.gap` (`reason: queue_overflow`; the `ws.terminal_stream.queue_overflow_spill` event is emitted at eviction time, rate-limited per connection), and only a sustained over-64 MiB backlog with zero successful socket sends for the whole stall window closes with 4008 (`ws.terminal_stream.catastrophic_close` carries per-occurrence `sends_in_window` evidence plus the lifetime `total_sends`).

**Claude Session Discovery:** Watches `~/.claude/projects/*/sessions/*.jsonl` for new files. Parses JSONL streams to extract messages, groups by project path.

Expand Down
22 changes: 22 additions & 0 deletions crates/freshell-freshagent/src/opencode_ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10087,6 +10087,28 @@ mod tests {
Some("ses_1"),
"fixture: the send materialized the durable id (and committed Live{{FreshAgent}})"
);
// The materialize spawn's ownership commit (Live{FreshAgent} on the
// durable key) lands asynchronously AFTER the durable id itself;
// on a contended 2-core CI runner the test can outrun it (observed
// twice on CI: begin_handoff met a not-yet-Live key and the fixture
// assert panicked — never on the 96-core dev box). Poll the
// registry until the committed precondition is observable instead
// of assuming the spawn completed; the bound trips only if the
// commit never lands at all.
let mut commit_polls = 0u32;
loop {
let snap = registry.observe("opencode", "ses_1");
if matches!(snap.state, freshell_ownership::OwnershipState::Live { .. }) {
break;
}
commit_polls += 1;
assert!(
commit_polls < 10_000,
"fixture: the materialize spawn never committed Live{{FreshAgent}} \
ownership for ses_1: {snap:?}"
);
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
}
// The refusal precondition: a handoff owns the durable key's
// transition, so the kill's fenced stop is typed-blocked.
let freshell_ownership::BeginOutcome::Granted { .. } = registry.begin_handoff(
Expand Down
111 changes: 110 additions & 1 deletion crates/freshell-protocol/src/client_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ pub enum ClientMessage {
TerminalAttach(TerminalAttach),
#[serde(rename = "terminal.interest")]
TerminalInterest(TerminalInterest),
/// Responsive-terminal-restore Workstream 1 (paced replay): the
/// continuation credit a `pacedTerminalReplayV1` client sends after fully
/// consuming an ordered replay page — `consumedSeq` is the last sequence
/// it consumed, `attachRequestId` scopes it to one attach generation.
/// Additive optional; protocol version stays 10. Ignored by servers that
/// predate the capability (accept-and-strip) and by connections whose
/// own hello did not negotiate it.
#[serde(rename = "terminal.replay.credit")]
TerminalReplayCredit(TerminalReplayCredit),
#[serde(rename = "terminal.autoResumeCancel")]
TerminalAutoResumeCancel(TerminalAutoResumeCancel),
#[serde(rename = "terminal.detach")]
Expand Down Expand Up @@ -121,7 +130,7 @@ pub enum ClientMessage {

/// The exact `type` discriminants of every client→server message, in the frozen
/// inventory's order. This is the T0 conformance checklist.
pub const CLIENT_MESSAGE_TYPES: [&str; 41] = [
pub const CLIENT_MESSAGE_TYPES: [&str; 42] = [
"amplifier.activity.list",
"claude.activity.list",
"client.diagnostic",
Expand Down Expand Up @@ -160,6 +169,7 @@ pub const CLIENT_MESSAGE_TYPES: [&str; 41] = [
"terminal.input",
"terminal.interest",
"terminal.kill",
"terminal.replay.credit",
"terminal.resize",
"ui.layout.sync",
"ui.screenshot.result",
Expand Down Expand Up @@ -187,6 +197,19 @@ pub struct HelloCapabilities {
/// advertises the capability back (§4.2). Absent for the frozen client.
#[serde(skip_serializing_if = "Option::is_none")]
pub pane_reconcile_v1: Option<bool>,
/// Paced terminal restore opt-in (responsive-terminal-restore Workstream
/// 1): the client understands bounded, ascending paced replay batches with
/// continuation credit. Additive optional — absent on the frozen client
/// and stripped-tolerant on older servers (no version bump).
#[serde(skip_serializing_if = "Option::is_none")]
pub paced_terminal_replay_v1: Option<bool>,
/// Hidden-pane lifetime claims (responsive-terminal-restore Workstream 1):
/// the client sends `terminal.interest.claimedTerminalIds` only after the
/// `ready` echo advertises the capability back. Additive optional — absent
/// on the frozen client and stripped-tolerant on older servers (no version
/// bump).
#[serde(skip_serializing_if = "Option::is_none")]
pub terminal_lifetime_claim_v1: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -240,6 +263,14 @@ pub struct TerminalInterest {
pub revision: u64,
pub focused_terminal_id: Option<String>,
pub visible_terminal_ids: Vec<String>,
/// Hidden-pane lifetime claims (responsive-terminal-restore Workstream 1,
/// negotiated `terminalLifetimeClaimV1` only): terminals this connection
/// wants kept alive WITHOUT attaching. `None` carries no claim information
/// (the frozen shape); `Some(set)` supersedes the connection's previous
/// claim set snapshot-by-snapshot — omitting an id from a later snapshot
/// is the explicit withdrawal (release).
#[serde(skip_serializing_if = "Option::is_none")]
pub claimed_terminal_ids: Option<Vec<String>>,
}

// --- client.diagnostic ------------------------------------------------------
Expand Down Expand Up @@ -376,6 +407,30 @@ pub struct TerminalAttach {
pub expected_session_ref: Option<SessionLocator>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_replay_bytes: Option<i64>,
/// Paced terminal restore (responsive-terminal-restore Workstream 1):
/// the negotiated forward-page limit — an optional UPPER BOUND on each
/// paced replay page's serialized bytes, honored only on
/// pacedTerminalReplayV1 connections and clamped to the server's own
/// page-budget cap (`min(requested, server cap)`). Round-2 finding F3:
/// the field used to be emitted by the client and silently stripped
/// here — it is now part of the honest wire contract. Additive
/// optional; a missing, malformed, or non-positive value falls back to
/// the server's default exactly like the pre-contract accept-and-strip
/// behavior (the lossy deserializer keeps a wrong-typed value from
/// failing the whole attach frame). Integer-valued number spellings
/// (`2048.0`, `2e3`) carry the same value as their canonical integer
/// forms and are accepted and validated the same way (E2R1 finding 3)
/// — never silently dropped. E2R1 finding 2 (the honest bound): pages
/// are bounded by max(requested, the atomic frame size) — a single
/// frame larger than the request forms its own ATOMIC single-frame
/// page, bounded by the server's fragment cap (every frame is
/// pre-fragmented, so one frame's serialized size never exceeds it).
#[serde(
default,
deserialize_with = "lossy_positive_i64",
skip_serializing_if = "Option::is_none"
)]
pub replay_page_bytes: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<TerminalAttachPriority>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -404,6 +459,60 @@ pub struct TerminalAttach {
pub observed_generation: Option<u64>,
}

/// [`TerminalAttach::replay_page_bytes`]'s lossy deserializer (round-2
/// finding F3): only a clean positive integer counts as a requested bound;
/// a missing, malformed (wrong-typed, fractional), or non-positive value
/// deserializes to `None` — the server's default — instead of failing the
/// whole attach frame. This preserves the pre-contract accept-and-strip
/// tolerance for buggy senders exactly.
///
/// E2R1 finding 3: integer-VALUED number spellings (`2048.0`, `2e3`)
/// deserialize through Serde JSON's float storage variant, but they carry
/// the same VALUE as their canonical integer spellings — JSON has one
/// number type, and the TS/Zod side (`z.number().int().positive()`) plus
/// the generated JSON Schema accept that value as an integer. The lossy
/// deserializer accepts and validates them exactly like the canonical
/// form; a protocol field a client emits is never silently ignored.
fn lossy_positive_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
Ok(value
.as_ref()
.and_then(positive_integer_value)
.filter(|n| *n > 0))
}

/// The positive-integer VALUE of one JSON number: Serde JSON's integer
/// storage directly, or its float storage when the value is integral
/// (E2R1 finding 3 — `2048.0`/`2e3` parse as floats). Fractional,
/// non-finite, non-positive, and out-of-i64-range values are `None` (the
/// malformed/non-positive fallback, never a wrong bound).
fn positive_integer_value(v: &serde_json::Value) -> Option<i64> {
let n = v.as_number()?;
if let Some(i) = n.as_i64() {
return Some(i);
}
let f = n.as_f64()?;
(f.is_finite() && f.fract() == 0.0 && f > 0.0 && f <= i64::MAX as f64).then_some(f as i64)
}

/// `terminal.replay.credit` (responsive-terminal-restore Workstream 1): one
/// continuation credit for a paced replay session, granted after the prior
/// page was consumed in order. `consumedSeq` must fall within the server's
/// outstanding-page window `(credited, lastSentPageEnd]`; stale generations
/// (a superseded `attachRequestId`) and out-of-window values are ignored.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TerminalReplayCredit {
pub terminal_id: String,
pub stream_id: String,
pub attach_request_id: String,
/// The last sequence the client fully consumed in order.
pub consumed_seq: i64,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TerminalDetach {
Expand Down
2 changes: 1 addition & 1 deletion crates/freshell-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub use settings::*;
pub const WS_PROTOCOL_VERSION: u32 = 10;

/// Every `type` discriminant the protocol speaks, both directions, sorted.
/// (41 client→server + 67 server→client = 108.)
/// (42 client→server + 67 server→client = 109.)
pub fn all_message_types() -> Vec<&'static str> {
let mut types: Vec<&'static str> = client_messages::CLIENT_MESSAGE_TYPES
.iter()
Expand Down
Loading
Loading