Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,34 @@ All notable changes to lcode are documented here. The format follows

## [Unreleased]

### Fixed

Found in a real session that spent an hour on an Android build whose downloads this machine
couldn't reach, then ended without a word:

- **A site this machine can't reach** now stops the guessing. lcode tracks, per site, the calls that
failed to reach it (HTTP errors, names that don't resolve, refused or timed-out connections) and
the ones that got an answer. After three different failed calls and none that worked, the model is
told to check one address that must exist and, if that fails too, to tell you what fails and what
you could do. Two more and lcode stops the request (status `blocked`). Before, every new address
looked like a new attempt, so nothing stepped in.
- **Thinking in circles** is stopped as it streams. A model that repeats the same few paragraphs
used to think until the context was full (here 70,000 characters, about ten minutes behind the
spinner). Now the reply is dropped once its sentences keep repeating, and the model is asked for
its next step or what's blocking it.
- **A reply with no answer** no longer ends the request as if it were done. An empty reply, or one
that filled the context before it said anything, gets a second chance; the second time lcode stops
(status `loop`) and offers the stronger model first, when one is set. An answer the context cut off
midway now says so. OpenAI-compatible servers report a cut-off reply too.
- **A `cd` out of the project** no longer moves lcode there. A model that ran `cd /tmp` took the
session, the project checks and the memory notes with it, and the session was saved under `/tmp`,
so `lcode -c` in the project couldn't find it. Now a `cd` within the project is kept, and one out
of it isn't: the next command starts in the project again. Sessions are saved under the folder
lcode works in.
- **Scripts it never ran** are sent back once before the answer counts: a model that wrote
`build_android.sh` and called the job done never ran it, and its first line failed. It now runs it
and fixes what fails, or says plainly that it's untested and why.

## [0.35.0] - 2026-10-14

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ went: `0` success, `1` an error, `3` stopped at `--max-steps`, `4` stopped becau

| Field | |
|---|---|
| `status` | `success`, `error`, `max_steps`, `loop` (the model kept repeating the same call) or `interrupted` |
| `status` | `success`, `error`, `max_steps`, `loop` (the model kept repeating the same call, or its replies went nowhere), `blocked` (a site it needed couldn't be reached) or `interrupted` |
| `text` | The model's final answer |
| `tool_calls` | Every tool call, with its arguments and output (the first 4,000 characters). `error` is true when the call failed or wasn't allowed |
| `files_changed` | Files the request added (`A`), modified (`M`) or deleted (`D`), shell commands included |
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ lcode config path # print the file location
| `escalate_model` | — | A [stronger model](usage.md#a-stronger-model-when-stuck) to ask, with your OK, when the model is stuck: `anthropic/…`, `openai/…`, `xai/…` (keys from `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`), `ollama/<model>`, or `custom/<model>` with `escalate_url` |
| `escalate_url` | — | Its server, when it isn't the provider's usual one: another Ollama, or any OpenAI-compatible API |
| `escalate_price` | — | Its price in dollars per million tokens, input/output (e.g. `3/15`), for the cost shown before each use |
| `repeat_limit` | `3` | Identical tool calls with identical results before lcode [steps in](how-it-works.md#when-the-model-repeats-itself); `0` turns it off |
| `repeat_limit` | `3` | Identical tool calls with identical results before lcode [steps in](how-it-works.md#when-the-model-repeats-itself); `0` turns it off, along with the checks for [unreachable sites](how-it-works.md#when-a-site-cant-be-reached) and [thinking in circles](how-it-works.md#when-a-reply-goes-nowhere) |
| `secret_check` | `true` | Before `git commit` and `git push`, look for passwords, keys and tokens they'd add ([details](usage.md#secrets-in-commits)) |
| `checkpoints` | `true` | Save a checkpoint before the model changes files, so [`/undo`](usage.md#undo-and-checkpoints) can restore them |

Expand Down
32 changes: 31 additions & 1 deletion docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,37 @@ A result that changes isn't a repeat, so a test that's run again after a fix, or
that's checked while it runs, never adds up. A call that may have changed something, like an edit or
a shell command that isn't read-only, starts the count again for the other calls: checking the build
after each edit is fine. Change the threshold with `lcode config set repeat_limit 5`, or turn it off
with `0`.
with `0` (that also turns off the checks for unreachable sites and for thinking in circles, below).

### When a site can't be reached

A download that fails however its address is written usually means this machine can't reach the
site: a firewall, a proxy, a regional block, or no internet. No address the model tries will fix
that, and every attempt differs, so the repeat count above never fires. lcode keeps track, per site,
of the calls that failed to reach it (an HTTP error, a name that doesn't resolve, a refused or
timed-out connection) and of the ones that got an answer.

- **When three different calls failed for a site and none worked,** the result gets a note: check
one address on that site that must exist, and if that fails too, stop and tell you what fails
and what you could do (another network or a VPN, a proxy, a mirror, or running that step
somewhere else, such as CI).
- **Two more,** and lcode ends the request with `Stopped: every request to dl.google.com failed (5
calls: HTTP 404)…`. The JSON output's status is `blocked`.

A site that answered once in the request is never counted as out of reach, so a wrong address on a
working site doesn't add up. Local addresses (localhost, private networks, names without a dot)
aren't tracked.

### When a reply goes nowhere

Small models sometimes think in circles: a few paragraphs, over and over, until the context is full.
That takes minutes behind the spinner and ends without an answer. lcode watches the thinking as it
streams, and when its sentences keep repeating, it stops the model, drops that reply and asks for a
short one: the next tool call, or what's blocking it. An empty reply, or one that filled the context
before it said anything, gets the same second chance. The second time in a request, lcode stops it
(status `loop`), offering the [stronger model](usage.md#a-stronger-model-when-stuck) first when one
is set. An answer the context cut off midway is kept, with a warning to `/compact` or use a larger
`/context`.

## Subagents

Expand Down
15 changes: 12 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ reasoning is on.
| `list_dir` | Directory tree, skipping `.git`, `node_modules`, virtualenvs and caches |
| `glob` | Find files by pattern, newest first |
| `grep` | Regex search with ripgrep (falls back to Python if ripgrep is missing) |
| `bash` | Run a shell command with live output, a timeout and a persistent working directory; with `background`, keep it running ([details](#background-commands)) |
| `bash` | Run a shell command with live output, a timeout and a persistent working directory (a `cd` within the project is kept, one out of it isn't); with `background`, keep it running ([details](#background-commands)) |
| `bash_output`, `bash_stop` | Read what a background command printed since last time; stop it |
| `todo_write` | Keep a visible task list for multi-step work, with notes (decisions, findings) that survive when old messages are removed; `/status` shows them |
| `web_search` | Search the web for current information (needs a [search provider](#web-search)) |
Expand Down Expand Up @@ -183,6 +183,13 @@ command = "uv run pytest -q && uv run ruff check ."
Or set it for yourself with `lcode config set checks "make check"`; `lcode config set checks off`
turns checks off.

**Scripts it never ran.** A model that writes a build or install script tends to call it done
without running it once, and the first thing you see is its first error. When a request wrote a
shell script (a `.sh` file, or a file with a shell shebang) and no command in it ran the script,
lcode sends it back once before the answer counts: run it and fix what fails, or say plainly that
it's untested and why, when running it would install, deploy or delete things. Making it executable
or reading it doesn't count as running it. `checks = "off"` turns this off too.

## A review before the answer

Models claim more than they did: half of a two-part request done, "it works now" without having
Expand Down Expand Up @@ -224,8 +231,10 @@ lcode config set escalate_price 3/15 # dollars per million tokens
```

**When.** lcode offers it when the [loop breaker](how-it-works.md#when-the-model-repeats-itself)
would stop the request, and when the project's [checks](#checks-before-done) fail twice in a row
after the model's fixes, at most once per request. You can also ask at any time: `/escalate`, or
would stop the request, when the model's replies [go nowhere](how-it-works.md#when-a-reply-goes-nowhere)
twice, and when the project's [checks](#checks-before-done) fail twice in a row after the model's
fixes, at most once per request. Not for a [site this machine can't reach](how-it-works.md#when-a-site-cant-be-reached):
a stronger model can't reach it either. You can also ask at any time: `/escalate`, or
`/escalate the window still grows` to say what's wrong.

**Nothing is sent without asking**, in any permission mode:
Expand Down
Loading
Loading