43b9ed13
review: pin what the blind path moved, and stop assuming leg 3's premise
a73x 2026-08-21 18:19
Commit message
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -5198,3 +5198,75 @@ form. e2e scenario 58 runs the nested case for real: inside a session nobody | |||
| 5198 | offered an agent to, a nested `mux -A` must exit 2 AND say so with the | 5198 | offered an agent to, a nested `mux -A` must exit 2 AND say so with the |
| 5199 | preflight's message, because the self-attach refusal standing behind it also | 5199 | preflight's message, because the self-attach refusal standing behind it also |
| 5200 | exits 2 and the code alone cannot tell which one spoke. | 5200 | exits 2 and the code alone cannot tell which one spoke. |
| 5201 | |||
| 5202 | ## 2026-08-21 (a detached session stops rendering, and what that moved) | ||
| 5203 | |||
| 5204 | Every number below is one box: idle 16-core x86_64, ReleaseSafe, `make | ||
| 5205 | throughput`'s own harness. They are quoted so a later reading has something | ||
| 5206 | to disagree with, not because they transfer. | ||
| 5207 | |||
| 5208 | **`make install` shipped Debug for the whole prototype's life.** `install` | ||
| 5209 | depended on `build`, which is `zig build` with no `-Doptimize`, so | ||
| 5210 | `~/.local/bin` got a Debug tree — and ghostty gates its page-integrity check | ||
| 5211 | on ITS OWN optimize mode, rebuilding a hash map over every cell and standing | ||
| 5212 | up a fresh DebugAllocator per page mutation. 20k lines of `yes` at 80×24, no | ||
| 5213 | client: **3713ms installed, 6ms ReleaseSafe, 6ms tmux, 24ms a bare pty.** | ||
| 5214 | perf put 96% of cycles in `verifyIntegrity`. The dev tree stays Debug on | ||
| 5215 | purpose — the assertions are worth having while working on the engine — so | ||
| 5216 | `install` and `throughput` each build ReleaseSafe into their own prefix | ||
| 5217 | rather than flipping the default. The rule that follows: **no speed number | ||
| 5218 | measured in the dev tree means anything**, and `make test`/`e2e`/`soak` all | ||
| 5219 | inherit Debug. | ||
| 5220 | |||
| 5221 | **A user-space profile misranked the thing it was used to rank.** The | ||
| 5222 | detached daemon's cost looked like the formatter (57% in | ||
| 5223 | `PageFormatter.writeCodepointWithReplacement`) with the allocator a ~5% | ||
| 5224 | footnote. The real split was `utime 24.1s / stime 106.5s` — **81% in the | ||
| 5225 | kernel**, from `dumpVtRow` taking a fresh GPA buffer per row, fifty per pty | ||
| 5226 | read, churning mmap/munmap and page faults. `perf record -p` cannot see it: | ||
| 5227 | those frames come back as unresolved `[unknown]` addresses. Removing the | ||
| 5228 | loop cut instructions 5.9× and wall time 27× (64784ms → 2361ms on 374MB of | ||
| 5229 | full-width repaint at 200×50); the missing 4.7× was system time. **Check the | ||
| 5230 | user/system split before ranking work off a profile.** | ||
| 5231 | |||
| 5232 | **The blind path owes three things `update()` used to provide.** `seq` must | ||
| 5233 | advance — it is stamped into `last_return.seq` and two commands returning | ||
| 5234 | during one blind stretch must not share one, or an await cannot tell which | ||
| 5235 | it was told about. Every `row_seq` must be stamped, so a reattach gets a | ||
| 5236 | delta carrying the whole grid rather than one omitting what moved unseen. | ||
| 5237 | And the hashes must be marked stale: blind output moves a row A→B, the | ||
| 5238 | reattach delta carries B, but the stored hash still says A — so a later B→A | ||
| 5239 | reads as "unchanged", nothing is sent, and the client shows B for the rest | ||
| 5240 | of the session. Only a change-and-change-back triggers it and nothing else | ||
| 5241 | in the tracker would catch it. | ||
| 5242 | |||
| 5243 | **It moved side-channel replay, and that was not intended.** `noteBlind` has | ||
| 5244 | no `.none` case, so a gap advances seq on every pty chunk. Events are | ||
| 5245 | stamped at `tracker.seq` and replayed strictly above the reattaching | ||
| 5246 | client's watermark, so a bare BEL or an OSC 52 with no redraw behind it — | ||
| 5247 | previously stamped at the seq the gap began on and therefore dropped — now | ||
| 5248 | survives a delta reattach. Better behaviour, arrived at sideways. Pinned in | ||
| 5249 | delta.zig rather than in the gap fixture, which prints a visible marker | ||
| 5250 | first precisely so it never depended on the seq rule. | ||
| 5251 | |||
| 5252 | **`hasClientsIn` counts a 0×0 attach, and must.** `muxa run`/`await` hold a | ||
| 5253 | client slot for the whole verb, so the win does not reach the agent | ||
| 5254 | workload — those sessions take the rendering path. The tempting narrowing, | ||
| 5255 | treating `cols == 0` as "not watching", is **unsafe**: the webhub stand-in | ||
| 5256 | attaches at 0×0 and reads real content back over its WebSocket — the e2e | ||
| 5257 | scenario whose comment reads "A POSTed tile is a REAL tile". 0×0 means | ||
| 5258 | "claims no grid", never "wants no bytes". Anchored on that sentence rather | ||
| 5259 | than a line number, which drifts. | ||
| 5260 | |||
| 5261 | **The throughput gate's bounds are calibrated, not principled.** Best-of-5 | ||
| 5262 | per leg, because noise only ever adds time and a regression moves the | ||
| 5263 | minimum as much as the mean. Solo 6ms/ceiling 10; client 45-60ms with a 60ms | ||
| 5264 | comfort target that only reports and a 75ms ceiling — a hard 60 flaked 1 run | ||
| 5265 | in 10 with nothing wrong, and a gate pinned to the noise floor gets disabled | ||
| 5266 | by its third false alarm. Repaint 87ms/ceiling 200, set near the good number | ||
| 5267 | rather than under the bad one: at 400 a machine twice as fast runs the | ||
| 5268 | BROKEN build in 240ms and the gate says nothing. All bounds are env | ||
| 5269 | overridable so a slower box can relax them without deleting the target. | ||
| 5270 | `yes` cannot see any of this — one-character rows are nearly free to render, | ||
| 5271 | which is why the regression this branch fixed was invisible to the first two | ||
| 5272 | legs (5ms vs 6ms) and obvious to the third (87ms vs 479ms). | ||
src/delta.zig
| Old | New | ||
|---|---|---|---|
| @@ -322,6 +322,50 @@ test "DeltaTracker: blind output is answerable on reattach without rendering a r | |||
| 322 | try std.testing.expect( | 322 | try std.testing.expect( |
| 323 | std.mem.indexOf(u8, composed.bytes, "printed with nobody watching") != null, | 323 | std.mem.indexOf(u8, composed.bytes, "printed with nobody watching") != null, |
| 324 | ); | 324 | ); |
| 325 | |||
| 326 | // The header, not only the rows. buildDeltaSince serialises the | ||
| 327 | // TRACKER's cursor and history_rows, so a blind path that stamped every | ||
| 328 | // row but forgot to refresh those two would repaint the right text with | ||
| 329 | // the cursor parked where the gap began. Deleting either line in | ||
| 330 | // noteBlind left the entire unit suite green until this assertion | ||
| 331 | // existed, and the client that would wear it is a browser reconnect — | ||
| 332 | // the CLI opens at seq 0 and takes the snapshot arm instead. | ||
| 333 | const hdr = try proto.readDeltaHeader(payload); | ||
| 334 | const cur = eng.cursorPos(); | ||
| 335 | try std.testing.expectEqual(cur.x, hdr.cursor_x); | ||
| 336 | try std.testing.expectEqual(cur.y, hdr.cursor_y); | ||
| 337 | try std.testing.expectEqual(eng.historyRows(), hdr.history_rows); | ||
| 338 | } | ||
| 339 | |||
| 340 | test "DeltaTracker: a blind chunk that changed nothing still advances seq" { | ||
| 341 | // Reads like a triviality; it is the premise two comments in server.zig | ||
| 342 | // now rest on. Side-channel events (OSC 52, a bell) are stamped at | ||
| 343 | // tracker.seq and replayed only when strictly ABOVE the reattaching | ||
| 344 | // client's watermark. update() answers .none for a chunk that moved no | ||
| 345 | // cell, which left a bare BEL during a gap stamped at the seq the gap | ||
| 346 | // began on and therefore never replayed — replayPending calls that out | ||
| 347 | // as the price it pays. noteBlind has no .none case, so during a gap | ||
| 348 | // those events now land above the watermark and survive the reattach. | ||
| 349 | // | ||
| 350 | // Deleting the unconditional `self.seq += 1` is what silently reverts | ||
| 351 | // that, and no server test would notice: the gap fixture prints a | ||
| 352 | // visible marker first precisely so it never depends on this. | ||
| 353 | const alloc = std.testing.allocator; | ||
| 354 | |||
| 355 | const eng = try Engine.init(alloc, .{ .cols = 80, .rows = 24 }); | ||
| 356 | defer eng.deinit(); | ||
| 357 | |||
| 358 | var tracker: DeltaTracker = .{}; | ||
| 359 | defer tracker.deinit(alloc); | ||
| 360 | try tracker.rebuild(alloc, eng, 24, 80); | ||
| 361 | |||
| 362 | // No feed at all: the strongest form of "this chunk changed nothing". | ||
| 363 | const before = tracker.seq; | ||
| 364 | switch (tracker.noteBlind(eng)) { | ||
| 365 | .advanced => {}, | ||
| 366 | else => return error.ExpectedAdvance, | ||
| 367 | } | ||
| 368 | try std.testing.expect(tracker.seq > before); | ||
| 325 | } | 369 | } |
| 326 | 370 | ||
| 327 | test "DeltaTracker: two blind stretches never share a seq" { | 371 | test "DeltaTracker: two blind stretches never share a seq" { |
src/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -3363,12 +3363,21 @@ pub const Server = struct { | |||
| 3363 | /// | 3363 | /// |
| 3364 | /// `> have_seq`, not `>=`: an event stamped at the seq the client already | 3364 | /// `> have_seq`, not `>=`: an event stamped at the seq the client already |
| 3365 | /// quotes is one it was there for, and replaying it would set the | 3365 | /// quotes is one it was there for, and replaying it would set the |
| 3366 | /// clipboard twice for a client that never missed anything. The price is | 3366 | /// clipboard twice for a client that never missed anything. |
| 3367 | /// that an event whose pty chunk changed no cell — a bare BEL, an OSC 52 | 3367 | /// |
| 3368 | /// with no redraw behind it — is stamped at the seq the gap began on and | 3368 | /// That used to cost an invisible chunk its replay: update() answers |
| 3369 | /// so is not replayed. Live delivery is unaffected; only the replay of an | 3369 | /// `.none` when no cell moved, so a bare BEL or an OSC 52 with no redraw |
| 3370 | /// invisible chunk is lost, and buying it back would mean handing out | 3370 | /// behind it stayed stamped at the seq the gap began on, and this loop |
| 3371 | /// duplicates to every client that was watching. | 3371 | /// refused it. It no longer does — and not by design. A gap is by |
| 3372 | /// definition unattached, `noteBlind` has no `.none` case, so every pty | ||
| 3373 | /// chunk during one advances seq and every event drained after it lands | ||
| 3374 | /// strictly above the departed client's watermark. Pinned in delta.zig | ||
| 3375 | /// ("a blind chunk that changed nothing still advances seq"), because | ||
| 3376 | /// nothing on this side would notice it going away again. | ||
| 3377 | /// | ||
| 3378 | /// The `>` still binds for an ATTACHED client, which is what it was | ||
| 3379 | /// written for: the alternative is handing duplicates to everyone who | ||
| 3380 | /// was watching, and that is still the wrong trade. | ||
| 3372 | fn replayPending(self: *Server, si: usize, i: usize, have_seq: u64) void { | 3381 | fn replayPending(self: *Server, si: usize, i: usize, have_seq: u64) void { |
| 3373 | const s = self.ses(si); | 3382 | const s = self.ses(si); |
| 3374 | // Enum declaration order, so clipboard precedes bell. Fixed rather | 3383 | // Enum declaration order, so clipboard precedes bell. Fixed rather |
| @@ -9115,12 +9124,19 @@ test "Server: a bell in a later chunk is its own frame, not folded into the firs | |||
| 9115 | /// - `gap-open`: removing it leaves the suite green and no mutation | 9124 | /// - `gap-open`: removing it leaves the suite green and no mutation |
| 9116 | /// uncovered. It removes a dependency instead; see its bullet. | 9125 | /// uncovered. It removes a dependency instead; see its bullet. |
| 9117 | /// | 9126 | /// |
| 9118 | /// - `gap-open` FIRST, and visible. Only a changed cell, a moved cursor or | 9127 | /// - `gap-open` FIRST, and visible. This was load-bearing back when only a |
| 9119 | /// history growth advances tracker.seq, so a burst of pure escapes can be | 9128 | /// changed cell, a moved cursor or history growth advanced tracker.seq: a |
| 9120 | /// drained at the same seq the departing client already held — and the | 9129 | /// burst of pure escapes could then drain at the same seq the departing |
| 9121 | /// replay is `> have_seq`, which would then refuse them. Printing | 9130 | /// client already held, and the replay is `> have_seq`, which would |
| 9122 | /// something visible before any escape guarantees every stamp below is | 9131 | /// refuse them. Printing something visible first put every stamp below it |
| 9123 | /// strictly above the watermark, whatever the pty happens to chunk. | 9132 | /// strictly above the watermark, whatever the pty happened to chunk. |
| 9133 | /// | ||
| 9134 | /// A gap is unattached, and the unattached path now advances seq on every | ||
| 9135 | /// chunk (`noteBlind`; the consequence is spelled out at replayPending), | ||
| 9136 | /// so the escapes would clear the watermark unaided. Kept regardless: it | ||
| 9137 | /// costs one printf, it keeps this fixture honest if the blind path ever | ||
| 9138 | /// regains a `.none`, and the dependency argument below never rested on | ||
| 9139 | /// the seq rule in the first place. | ||
| 9124 | /// | 9140 | /// |
| 9125 | /// Removing it leaves the suite GREEN, and that is not an argument for | 9141 | /// Removing it leaves the suite GREEN, and that is not an argument for |
| 9126 | /// removing it. What it removes is a DEPENDENCY: without it these tests | 9142 | /// removing it. What it removes is a DEPENDENCY: without it these tests |
test/throughput.sh
| Old | New | ||
|---|---|---|---|
| @@ -38,11 +38,19 @@ MUXA="$3" | |||
| 38 | PTYCLIENT="$4" | 38 | PTYCLIENT="$4" |
| 39 | 39 | ||
| 40 | # Lines of `yes`, and the wall-clock ceiling each leg must come in under. | 40 | # Lines of `yes`, and the wall-clock ceiling each leg must come in under. |
| 41 | # | ||
| 42 | # Every bound is overridable from the environment. All of them were | ||
| 43 | # calibrated on one idle 16-core box, and `make ci` now gates delivery on | ||
| 44 | # them, so a slower machine has to be able to relax the gate without | ||
| 45 | # editing this file — otherwise the only move left is deleting the target, | ||
| 46 | # and a deleted gate catches nothing. SOLO_MAX_MS carries the thinnest | ||
| 47 | # margin of the three: 10ms against an observed 6ms, on a bracket that | ||
| 48 | # includes `yes | head` fork and pipe cost. | ||
| 41 | SOLO_LINES=20000 | 49 | SOLO_LINES=20000 |
| 42 | SOLO_MAX_MS=10 | 50 | SOLO_MAX_MS=${SOLO_MAX_MS:-10} |
| 43 | CLIENT_LINES=200000 | 51 | CLIENT_LINES=200000 |
| 44 | CLIENT_WARN_MS=60 | 52 | CLIENT_WARN_MS=${CLIENT_WARN_MS:-60} |
| 45 | CLIENT_MAX_MS=75 | 53 | CLIENT_MAX_MS=${CLIENT_MAX_MS:-75} |
| 46 | # Leg 3 repaints a full screen over and over with NOBODY attached. Its | 54 | # Leg 3 repaints a full screen over and over with NOBODY attached. Its |
| 47 | # own leg because `yes` cannot see what it sees: a one-character row is | 55 | # own leg because `yes` cannot see what it sees: a one-character row is |
| 48 | # nearly free to render, so the solo leg above stays flat through a | 56 | # nearly free to render, so the solo leg above stays flat through a |
| @@ -50,7 +58,7 @@ CLIENT_MAX_MS=75 | |||
| 50 | # vim, logs, a build — is full-width, and the daemon renders every row | 58 | # vim, logs, a build — is full-width, and the daemon renders every row |
| 51 | # of it to hash it. | 59 | # of it to hash it. |
| 52 | # 2^REPAINT_DOUBLINGS frames of ~1.8KB; 13 is ~15MB. | 60 | # 2^REPAINT_DOUBLINGS frames of ~1.8KB; 13 is ~15MB. |
| 53 | REPAINT_DOUBLINGS=13 | 61 | REPAINT_DOUBLINGS=${REPAINT_DOUBLINGS:-13} |
| 54 | # Measured either side of the fix, same box, best-of-5: 87ms with the | 62 | # Measured either side of the fix, same box, best-of-5: 87ms with the |
| 55 | # detached check in place, 479ms without it. The ceiling sits near the | 63 | # detached check in place, 479ms without it. The ceiling sits near the |
| 56 | # good number rather than under the bad one — put it at 400 and a machine | 64 | # good number rather than under the bad one — put it at 400 and a machine |
| @@ -59,13 +67,22 @@ REPAINT_DOUBLINGS=13 | |||
| 59 | # | 67 | # |
| 60 | # 5.5x here against 27x at 200x50: the penalty is per rendered row, and | 68 | # 5.5x here against 27x at 200x50: the penalty is per rendered row, and |
| 61 | # this leg renders 23 of 80 columns rather than 49 of 200. | 69 | # this leg renders 23 of 80 columns rather than 49 of 200. |
| 62 | REPAINT_MAX_MS=200 | 70 | REPAINT_MAX_MS=${REPAINT_MAX_MS:-200} |
| 63 | # Repeats per leg; the fastest one is the reading. See the header. | 71 | # Repeats per leg; the fastest one is the reading. See the header. |
| 64 | REPS=5 | 72 | REPS=${REPS:-5} |
| 65 | 73 | ||
| 66 | TMPD="$(mktemp -d "${TMPDIR:-/tmp}/muxd-throughput-XXXXXX")" | 74 | TMPD="$(mktemp -d "${TMPDIR:-/tmp}/muxd-throughput-XXXXXX")" |
| 67 | SOCK="$TMPD/t.sock" | 75 | SOCK="$TMPD/t.sock" |
| 68 | cleanup() { kill "$DPID" 2>/dev/null || true; rm -rf "$TMPD"; } | 76 | # Assigned BEFORE the trap is installed, not after the daemon launches: |
| 77 | # cleanup dereferences it under `set -u`, so a trap firing in between would | ||
| 78 | # die on an unbound variable and never reach the rm. Guarded with `if` | ||
| 79 | # rather than `&&` because a failing AND-OR list as a function's first | ||
| 80 | # statement is exactly the shape `set -e` is entitled to act on. | ||
| 81 | DPID="" | ||
| 82 | cleanup() { | ||
| 83 | if [ -n "$DPID" ]; then kill "$DPID" 2>/dev/null || true; fi | ||
| 84 | rm -rf "$TMPD" | ||
| 85 | } | ||
| 69 | trap cleanup EXIT INT TERM | 86 | trap cleanup EXIT INT TERM |
| 70 | 87 | ||
| 71 | "$MUXD" run --sock "$SOCK" --shell /bin/sh --cols 80 --rows 24 & | 88 | "$MUXD" run --sock "$SOCK" --shell /bin/sh --cols 80 --rows 24 & |
| @@ -137,7 +154,7 @@ RC=0 | |||
| 137 | -- "$MUX" --sock "$SOCK" < "$TMPD/script" > "$TMPD/log" 2>&1 || RC=$? | 154 | -- "$MUX" --sock "$SOCK" < "$TMPD/script" > "$TMPD/log" 2>&1 || RC=$? |
| 138 | [ "$RC" -eq 0 ] || { | 155 | [ "$RC" -eq 0 ] || { |
| 139 | echo "throughput FAIL: client leg exited $RC" | 156 | echo "throughput FAIL: client leg exited $RC" |
| 140 | echo " (3 = the floods never completed inside 60s)" | 157 | echo " (3 = the floods never completed inside the expect's 90s)" |
| 141 | cat "$TMPD/log" | 158 | cat "$TMPD/log" |
| 142 | exit 1 | 159 | exit 1 |
| 143 | } | 160 | } |
| @@ -166,7 +183,27 @@ echo "client: $CLIENT_LINES lines, attached: ${CLIENT_MS}ms (target ${CLIENT_WA | |||
| 166 | } | 183 | } |
| 167 | 184 | ||
| 168 | # ---- leg 3: full-width repaint, nobody attached ------------------------ | 185 | # ---- leg 3: full-width repaint, nobody attached ------------------------ |
| 169 | # The client from leg 2 has quit, so this is a detached session again. | 186 | # "Nobody attached" is this leg's entire meaning, so it is asserted rather |
| 187 | # than assumed. Leg 2's `waitexit` proved the mux PROCESS is gone; it did | ||
| 188 | # not prove the daemon has reaped the client slot, and those are different | ||
| 189 | # instants. Measured with a slot still open, this leg would be timing the | ||
| 190 | # attached path against a detached ceiling and failing for the wrong | ||
| 191 | # reason. The greedy `.*` takes the LAST clients= in the line, which is the | ||
| 192 | # per-session count rather than the daemon-wide one. | ||
| 193 | i=0 | ||
| 194 | while [ "$i" -lt 50 ]; do | ||
| 195 | if [ "$("$MUXD" stats --sock "$SOCK" | sed -n 's/.*clients=\([0-9]*\).*/\1/p')" = "0" ]; then | ||
| 196 | break | ||
| 197 | fi | ||
| 198 | sleep 0.1 | ||
| 199 | i=$((i+1)) | ||
| 200 | done | ||
| 201 | [ "$i" -lt 50 ] || { | ||
| 202 | echo "throughput FAIL: leg 2's client is still attached, so leg 3 would not" | ||
| 203 | echo " be measuring a detached session at all" | ||
| 204 | "$MUXD" stats --sock "$SOCK" | ||
| 205 | exit 1 | ||
| 206 | } | ||
| 170 | # | 207 | # |
| 171 | # One frame: cursor home, then 23 rows of 79 columns. 23 and not 24 so the | 208 | # One frame: cursor home, then 23 rows of 79 columns. 23 and not 24 so the |
| 172 | # screen is REPAINTED rather than scrolled — scrolling is a different cost | 209 | # screen is REPAINTED rather than scrolled — scrolling is a different cost |
| @@ -200,8 +237,14 @@ while [ ! -s "$TMPD/repaint.done" ] && [ "$i" -lt 120 ]; do sleep 0.5; i=$((i+1) | |||
| 200 | echo "throughput FAIL: $REPS repaints of $REPAINT_BYTES bytes unfinished after 60s" | 237 | echo "throughput FAIL: $REPS repaints of $REPAINT_BYTES bytes unfinished after 60s" |
| 201 | exit 1 | 238 | exit 1 |
| 202 | } | 239 | } |
| 203 | REPAINT_ALL="$(cat "$TMPD/repaint")" | 240 | REPAINT_ALL="$(cat "$TMPD/repaint" 2>/dev/null || true)" |
| 204 | REPAINT_MS="$(echo "$REPAINT_ALL" | sort -n | head -1)" | 241 | REPAINT_MS="$(echo "$REPAINT_ALL" | sort -n | head -1)" |
| 242 | # Same two guards as leg 2's, in the same order. Unreachable given the | ||
| 243 | # `-s repaint.done` poll above, but an asymmetry between twins is an | ||
| 244 | # invitation to "fix" whichever one someone reads second. | ||
| 245 | [ -n "$REPAINT_MS" ] || { | ||
| 246 | echo "throughput FAIL: repaint leg recorded no timing"; exit 1; | ||
| 247 | } | ||
| 205 | [ "$(echo "$REPAINT_ALL" | wc -l)" -eq "$REPS" ] || { | 248 | [ "$(echo "$REPAINT_ALL" | wc -l)" -eq "$REPS" ] || { |
| 206 | echo "throughput FAIL: wanted $REPS repaint readings, got: $REPAINT_ALL" | 249 | echo "throughput FAIL: wanted $REPS repaint readings, got: $REPAINT_ALL" |
| 207 | exit 1 | 250 | exit 1 |