@turns/dev
v0.0.22
Published
Upload local AI agent sessions to turns.dev
Readme
Turns CLI
turns uploads native agent transcripts directly from this machine to the
current user's Durable Object on turns.dev. Transcript rows are secret-filtered
on the machine immediately before the WebSocket boundary; local transcript
files are never rewritten.
Install the CLI with Node.js 22.18 or newer:
npm install --global @turns/devCommands
turns watch and stream sessions for the current directory
turns upload [directory] watch and stream sessions for one directory
turns --once [directory] upload that directory's sessions once and exit
turns install install the all-directory user daemon
turns status report daemon state
turns uninstall remove the daemon
turns login replace saved credentials through the browser
turns logout remove saved credentialsturns and turns upload DIRECTORY are the daemon scoped to one directory
tree: they keep watching, stream live rows as the harness writes them, and
serve prompts queued from the web until interrupted. That directory is the
allowlist, so sessions in its subdirectories are followed and agent launches
cannot start outside it. Run turns install instead for an unattended
all-directory daemon; while that daemon is active a foreground run declines to
start a second uploader, since both would write the same upload-cursor file.
--once sweeps the same directory tree and exits as soon as its sessions are
current, for scripts and CI steps that need an upload to finish rather than a
process to stay resident. A run narrowed to a directory — watching or --once —
never prunes saved upload cursors, so it cannot make the sessions it filtered
out replay from their first byte the next time the daemon starts.
Native background tasks retain their detached agent anchor after the foreground prompt finishes, including while the daemon client is disconnected. Codex child agents, Claude/Letta tasks, ACP task tools, Prime/OMP jobs, and DeepSeek jobs suspend the normal five-minute idle cleanup. Follow-up prompts remain available; the anchor starts a fresh idle countdown after the last known task finishes or its transport exits. Explicit shutdown, interruption, transport replacement, and turn timeouts retain their existing semantics.
Retention uses native lifecycle events and task-tool results. Assistant prose, a completed launch tool, or a failed status query does not prove that a task has finished. Copilot, Hermes, and OpenCode captures lack reliable terminal evidence in their live streams, so their anchors can stay resident until a later native status result, transport exit, or explicit shutdown. No time-based fallback kills quiet tasks. One-shot harnesses such as Muse and Antigravity already remain foreground-running until their native process exits; there is no warm child process for idle cleanup to kill afterward.
The server is selected with --server URL or TURNS_ORIGIN. Its normalized
origin is saved and reused by later invocations and daemon starts. On Linux,
Turns uses the XDG config/state directories and installs this user service:
~/.config/turns/machine-id~/.local/state/turns/native-upload-state.json~/.local/state/turns/letta-sessions/*.json(Letta conversation-to-project associations)~/.config/systemd/user/turns.service
On macOS, credentials and daemon state live under
~/Library/Application Support/turns/, and turns install creates the per-user
LaunchAgent ~/Library/LaunchAgents/dev.turns.cli.plist. Its output is written
to ~/Library/Application Support/turns/daemon.log.
The credential file contains a seven-day access token and a signed refresh
token. The CLI refreshes the access token at least once every 24 hours. A new
machine opens a one-time turns.dev URL, waits on an authentication WebSocket,
and saves the resulting pair with mode 0600. Set TURNS_NO_BROWSER=1 to only
print the URL.
Rows are scanned on their way out for substrings that look like file paths —
out/chart.png, `src/app.tsx`, ~/shots/a.png — and each one the
filesystem confirms is recorded on the session's own log as a resolution: the
substring, the absolute path, the size, and whether the contents were sent.
Paths resolve against the session's working directory and are canonicalised
before the --directory prefixes are enforced, so a symlink out of an
allowed tree is not a way to read the disk.
Only images under 10 MB are uploaded, to PUT /api/file/{machine}/{path}. Everything else is recorded as a name, so
a reader learns what a response was pointing at without the machine shipping
source files. Nothing is claimed until the filesystem confirms it, which is what
bounds a deliberately broad matcher: prose that merely looks like a path
resolves to nothing and is never mentioned to the server, and no directory is
ever scanned.
An explicit file browse may upload up to 2 GiB to the same private R2 prefix. Files over 100 MiB use retryable 32 MiB multipart requests and report upload progress to the requesting browser over the control WebSocket. The daemon streams the file from disk and the Worker hands that request stream straight to R2, so neither process retains the complete file in memory.
Range-based viewers can bypass that upload. A capable daemon advertises live file ranges, validates the path through the same allowlist, and streams a bounded window as ordered 32 KiB WebSocket chunks. The browser hex view pages 64 KiB viewport windows, including arbitrary offset jumps.
The systemd unit uses KillMode=process, and the macOS LaunchAgent uses
AbandonProcessGroup: restarting the uploader leaves its detached agent-anchor
processes running so in-flight harness turns continue.
For a foreground daemon restricted to one or more directory trees, repeat
--directory. Both transcript discovery and agent launches enforce the
canonical prefix, including protection against symlinks that escape it:
turns daemon run --server https://turns.example --directory /srv/agentsAgainst local Wrangler development, use the configured DEV_USER directly:
turns daemon run --server http://127.0.0.1:8787 \
--dev-user antimatter15 --directory /tmp/turns-driver-workspacesAgent memory isolation and prompt recovery
On Linux with systemd, each newly launched anchor runs in its own
turns-agent-*.scope, including all of its harness and tool subprocesses.
The installer also writes turns-agents.slice, which contains these scopes
and enables systemd-oomd pressure monitoring at 40%. This targets an individual
agent scope before pressure reaches the user manager's usual 50% threshold.
Foreground launches still get individual scopes, but the
parent slice's pressure policy requires running turns install.
OOMPolicy=kill terminates that entire scope when a member is OOM-killed;
other anchors and the uploader live in different cgroups. Turns imposes no
per-agent RAM or swap limit: workloads share available host resources, and
pressure handling selects an individual scope for termination. Scope creation requires an
accessible systemd user manager, cgroup v2 memory control, and systemd 254 or
newer. A launch failure is reported instead of silently running without
isolation. macOS and Linux without systemd retain detached process launching.
The Linux service records the installation PATH. Executable discovery and
anchor environments also add the CLI's Node directory, ~/.local/bin,
~/bin, ~/.npm-global/bin, NPM_CONFIG_PREFIX/bin when configured, and
standard system/Homebrew directories. Explicit PATH entries retain priority;
TURNS_<HARNESS>_BIN overrides and macOS app-bundled Codex preference remain
in effect. Empty and relative PATH entries are excluded. Keeping Node on the
anchor PATH also supports CLI wrappers using #!/usr/bin/env node.
Queue dispatch now distinguishes optimistic transcript acknowledgement from
durable delivery. New daemons request dispatchTracking when claiming and
wait for queue.renew with dispatchStarted: true to be acknowledged before
sending to the local anchor. The backend stores pending or started in
prompt_queue.dispatch_state. A failure, lost anchor, or expired lease consumes
a started queue entry; its transcript remains, and a user can submit a new
follow-up to continue. Unsent prompts retain the existing paused/retry behavior.
Surviving anchors retain their claims. A crash between the durable marker and
local delivery is deliberately treated as uncertain delivery and is not
replayed automatically.
Deploy the backend before upgrading daemons. User DO schema version 20 adds
dispatch_state without changing existing columns. Existing rows and claims
from older daemons use legacy: acknowledged legacy claims are conservatively
treated as started, since old daemons did not record a separate delivery
boundary. This can consume a legacy optimistic-but-unsent prompt on failure;
it prevents replay of work with unknown delivery state. New daemons refuse to
send against backends that do not advertise dispatch tracking. Downgrade
new daemons and drain active claims before rolling back the backend; an old
backend ignores the new column and cannot provide the no-replay guarantee.
Reinstall the Linux service with turns install after upgrading to persist its
PATH. Existing warm anchors keep their old executable, environment, and cgroup
until they exit; isolation applies to newly launched anchors. Previously
requeued prompts are not automatically deleted by this migration.
