Joseph Cox at 404 Media reported on September 14 that OpenAI runs a program called Project Lily, in which hundreds of contractors read whole conversations between users and ChatGPT and grade the replies. The contractors do not see usernames. OpenAI told 404 Media it tries to strip personal information before a conversation reaches a reviewer, and acknowledged that sensitive details still get through.
Every service keeps what you sent it. What stands out here is that "we try to remove personal information" is the whole guarantee, and it is a guarantee about effort rather than about what the service is able to read.
This week shell.online started keeping more than it used to. A change merged on September 16 makes the relay hold the last screen of every session. That is new retention, so this post is about what exactly is held, for how long, and what the relay can do with it. All of it is in the shell.online repository, mostly in worker/index.ts.
The bug that caused it
A viewer's connection and the machine's connection are two different things. A browser can be perfectly attached to a session whose host rebooted, closed its lid, or lost Wi-Fi, and until this week both of our viewers showed that as a blank terminal. The relay had always sent the machine's state on every status message. The standalone page turned it into a grey "Offline" dot and nothing else. The app pane, per the pull request that fixed it, only read the exited status and dropped everything else, so it stayed on "connected" and drew an empty rectangle.
For a session that hosts a coding agent, that is a bad failure. The agent's machine goes away for ten minutes and the person watching the browser link for that process cannot tell an idle prompt from a dead host.
What the relay now stores
The fix has two halves. The first is a small module, shared/host-presence.ts, vendored into both the standalone page and the app so they say the same words. A session whose machine never connected reads "Waiting for this machine". One whose machine dropped reads "Temporarily offline", with when it was last seen and, if there is a kept screen under the notice, how old that screen is. The kept screen is drawn at 55 percent opacity so nobody reads it as live, and the notice clears itself when the host reconnects.
The second half is the retention. When a host sends a full screen snapshot, the relay now writes it to Durable Object storage. Durable Object values stop at 128 KiB and a snapshot can reach 512 KB, so it is stored in 64 KiB chunks under one key prefix. A busy room used to produce one snapshot per arriving viewer, so writes are throttled to one every two seconds. And because a host only produces a screen when asked, the relay's alarm asks for one whenever the kept copy is older than five minutes, addressed to viewer id 0, which no real viewer is ever given. That caps staleness at the cost of one snapshot per five minutes of a live session.
A viewer that arrives while the machine is away gets that screen replayed, once on join and once more after the password gate, since an encrypted frame is useless to a browser that cannot decrypt it yet.
What the relay can read
Here is where the comparison with Project Lily stops being a stretch. For an end-to-end encrypted session, which is the default, the bytes the relay stores are the same ciphertext it would have forwarded. The frame's opcode is authenticated as associated data, so the relay cannot even re-frame the snapshot. It stores the bytes and replays them untouched. It has no key, so there is no "we try to remove personal information" step because there is nothing for it to remove or to see.
That is not a promise about effort. It is a property of the relay's cacheScreen function, and anyone with the repository can check it.
There is one honest exception. A session started with --no-e2ee sends plaintext over TLS, and its kept screen is plaintext on the relay for as long as the session exists. That flag already makes the same tradeoff in flight, so the change adds a resting copy of what was already readable in transit. Two limits apply. The CLI refuses --persistent together with --no-e2ee, so a plaintext screen can only live as long as a normal session. And normal sessions expire after twelve hours, persistent ones after thirty days, and the screen chunks are deleted with everything else in the session's storage at exit or expiry. Both lifetimes are constants in shared/session-lifetime.ts.
We are stating the retention plainly because it is the kind of thing that gets found later by someone reading the code. The pull request carries a retention note for the same reason.
The rest of 0.16.0
The kept screen landed after the v0.16.0 tag, which shipped the same week. The release adds shell --name <name> <command> to label a session as it starts, with the name kept across restarts of a persistent session. It also adds shell ls, which lists the sessions in your account from every linked machine, hides ended ones behind --all, and prints full records with --json but never a password. Sessions can be renamed from their page in the web app by the owner, an assignee, or a team admin. A Download my data action on the Account page exports the account, its membership, linked machines, owned or assigned sessions, and the encrypted vault record as a JSON file. When a service extends what it keeps, the export is the place a user should be able to see that, and the session records are in it.
Pilot Protocol builds shell.online, and the same rule applies across the agent overlay network: peers hold the keys, relays forward bytes they cannot read, and any retention a relay adds is retention of ciphertext. The Pilot Protocol source follows the same X25519 and AES-256-GCM construction for its tunnels. If a relay in the path started keeping the last frame of a tunnel tomorrow, the honest disclosure would be the same as this one: here is what is kept, here is how long, and here is why it does not matter who reads it.
Top comments (0)