Skip to content

Add max_run_after_disconnect exec option - #41

Open
gkpacker wants to merge 6 commits into
superfly:mainfrom
gkpacker:gkpacker/max_run_after_disconnect-option
Open

gkpacker wants to merge 6 commits into
superfly:mainfrom
gkpacker:gkpacker/max_run_after_disconnect-option

Conversation

@gkpacker

Copy link
Copy Markdown

Why

The exec WebSocket (/v1/sprites/{name}/exec) accepts a max_run_after_disconnect query param. The API docs mention it ("Commands continue running after disconnect; use max_run_after_disconnect to control timeout"), but no SDK exposes it. Without it, a spawned command keeps running indefinitely after the client disconnects, even after a clean close. Checked against the live API on 2026-10-16:

  • No param: a bash TTY session was still listed in GET /v1/sprites/{name}/exec and ps more than 5 minutes after the socket closed.
  • max_run_after_disconnect=10s with an abrupt socket drop: a background loop stopped about 9s later and the session left the list.

What changed

  • New :max_run_after_disconnect exec option, a duration string such as "10s" or "5m". Omitted by default, so current behaviour (the command keeps running, no time limit) is unchanged.
  • Direct mode sends it as a query param on spawn only. Attach URLs don't include it, since the server ignores spawn options on attach.
  • Control mode sends it in the op.start exec args under the same key, so both transports behave the same.
  • Documented in Sprites.cmd/4, Sprites.spawn/4, the Sprites.Command moduledoc and the README, e.g. Sprites.spawn(sprite, "bash", [], tty: true, max_run_after_disconnect: "10s").
  • Refactored the private control-mode args builder into small steps (no behaviour change).
  • Tests: adds a reusable test/support/fake_ws_server.ex (a small GenServer-based WebSocket server, with its own tests) and an ExecCase template, plus end-to-end tests for spawn, cmd, attach_session and control-mode exec covering existing options as well as the new one. mix.exs gains elixirc_paths for the test env.

Only strings are accepted for now. Integer milliseconds can be added once the server is confirmed to accept an ms suffix.

One in roughly 50 local runs hit a pre-existing race in Control.ensure_table_owner (concurrent first callers racing to create the shared ETS tables); it also exists on main and is left alone here.

Note

sprites-go doesn't expose this option either: neither its exec query builder (exec.go) nor its control op.start args (websocket.go) set max_run_after_disconnect.

🤖 Generated with Claude Code

gkpacker and others added 3 commits October 16, 2026 19:04
Adds test/support (compiled in the test env only) with a small
GenServer-based WebSocket server standing in for the Sprites exec and
control endpoints, an ExUnit case template with helpers to spawn against
it, and tests for the server itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Splits Command.build_control_args/4 into small put_control_* steps,
matching the add_*_param helpers in Sprite. No behaviour change; adds
end-to-end tests for direct and control-mode exec requests covering the
existing options.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Exposes the exec endpoint's max_run_after_disconnect query param as a
duration string (e.g. "10s"). Direct mode sends it on spawn only; control
mode sends it in the op.start args under the same key. Omitted when unset,
so default behaviour is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T22:14:46.594476Z 75a8fc5 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75a8fc5b11

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/sprites/command.ex

Map.put(control_args, "stdin", if(stdin, do: "true", else: "false"))
defp put_control_max_run_after_disconnect(control_args, opts) do
case Sprite.max_run_after_disconnect(opts) do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate duration before checking out control connections

When control mode is selected and a non-string value is passed, Control.checkout/1 has already marked conn_pid busy before this validation raises. The init process then exits before ControlConn.start_op/4 and never executes Control.checkin/2, so the still-live connection remains marked busy; repeated rejected spawns consume sockets and can fill the 100-slot pool, forcing later valid control commands down the direct fallback. Validate before checkout or check the connection back in on this failure path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed in 0fd982c: the option is now validated in Command.start/4 before the GenServer starts, so no checkout happens and callers get an ArgumentError directly; the test asserts neither transport is contacted.

Comment thread test/support/fake_ws_server.ex Outdated
upgrade(socket, headers, state.owner)

for frame <- state.frames,
do: :ok = :gen_tcp.send(socket, [<<0x82, byte_size(frame)>>, frame])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Encode extended lengths for configured frames

When a configured server-to-client frame is 126 bytes or larger, this writes its raw size into the one-byte short-length header. Values 126 and 127 are sentinels requiring subsequent extended-length fields, while larger values also misuse or overflow that header, so gun parses payload bytes as framing instead of delivering the intended output. Encode the 16-bit and 64-bit WebSocket length forms before sending.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0398e8d: frames now use the 16-bit/64-bit length forms, and the server test sends 300-byte and 70,000-byte frames through gun.

Comment thread test/support/exec_case.ex
@@ -0,0 +1,78 @@
defmodule Sprites.Test.ExecCase do

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some tests for all options

@@ -0,0 +1,144 @@
defmodule Sprites.Test.FakeWsServer do

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH this was claude coded - I did add some tests for it and as it was test code, I thought it was better to add more coverage than not

gkpacker and others added 2 commits October 16, 2026 19:17
Server-to-client frames of 126+ bytes used the one-byte length field,
which gun parsed as framing. Use the 16-bit and 64-bit length forms and
cover both in the server's tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
In control mode the option was validated after Control.checkout/1, so a
bad value crashed init with the connection still checked out. Validate
in Command.start/4 instead, so callers get an ArgumentError and no
connection is touched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gkpacker added a commit to gkpacker/sprites-ex that referenced this pull request Oct 17, 2026
…ect (superfly#41)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gkpacker added a commit to gkpacker/sprites-ex that referenced this pull request Oct 17, 2026
…ect (superfly#41)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gkpacker added a commit to gkpacker/sprites-ex that referenced this pull request Oct 17, 2026
…ect (superfly#41)

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

@aezell aezell left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gkpacker The change looks good. There's a small nuance that might be good to make in the documentation.

The new docs say omitting max_run_after_disconnect allows commands to run indefinitely after disconnect, but the server defaults non-TTY commands to 10 seconds; only TTY commands default to unlimited runtime. Since tty: false is the SDK default, could you update both cmd/4 and spawn/4 docs to distinguish these defaults?

@gkpacker

Copy link
Copy Markdown
Author

Sure, on it @aezell!

Non-TTY commands (the SDK default) stop 10 seconds after disconnect;
only TTY commands run with no time limit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gkpacker

Copy link
Copy Markdown
Author

@aezell Thanks, good nuance. Updated in 24cc590: cmd/4, spawn/4 and the README now say non-TTY commands (the default) stop 10s after disconnect and only TTY commands run with no limit.

@gkpacker

Copy link
Copy Markdown
Author

@aezell done - my claude also decided to answer for me 🤣

gkpacker added a commit to gkpacker/sprites-ex that referenced this pull request Oct 26, 2026

This branch has not been deployed

No deployments
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