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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
BMP, ICO and PDF up to 20 MB per side; the Pulls screen still says "Binary
file".

- **A turn your plan's usage limit stopped picks up again once the limit
resets.** On Claude Code and Codex, the card says which limit stopped the
session and when it lifts, and lich types a prompt asking the agent to
continue two minutes after the reset. Any turn that starts before then, yours
or the provider's own, cancels it, and so does **Scheduled…** › **Cancel it**
in the card's menu. Nothing is parked for a limit that resets more than a day
out. Claude Code needs the lich-plugin release that reports `StopFailure`.

### Changed

- **The window is laid out as islands.** The session sidebar, the terminals and
Expand All @@ -62,6 +70,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
worktree's long path no longer stretches the cards wider than the sidebar and
pushes the rest of the list sideways while the drag is in flight.

- **A Codex card no longer spins forever after a usage limit ends its turn.**
Codex reports no end for that turn, so lich now reads it off the session's own
record.

## [0.61.0] - 2026-10-07

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ reads what each CLI writes down and no two of them write down the same things.
| Context window in the footer | yes | yes | no | no | no | no | no | yes |
| Cost in the footer | yes | yes | no | yes | yes | yes | no | credits |
| How much of your plan is left | yes | yes | no | no | no | no | no | no |
| A turn your plan's usage limit stopped picks up again once the limit resets | yes | yes | no | its own retries | its own retries | no | no | no |
| Spinner while a turn runs, ring when it ends | yes | yes | yes | yes | yes | no | no | yes |
| Bell when the agent is blocked on you | yes | yes | no | yes | no | no | no | no |
| Sidebar filter by state: Waiting, Running, Unread | yes | yes | no Waiting | yes | no Waiting | no, always Idle | no, always Idle | no Waiting |
Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
| 底栏里的上下文窗口 | 有 | 有 | 无 | 无 | 无 | 无 | 无 | 有 |
| 底栏里的花费 | 有 | 有 | 无 | 有 | 有 | 有 | 无 | 按点数计 |
| 套餐额度还剩多少 | 有 | 有 | 无 | 无 | 无 | 无 | 无 | 无 |
| 被套餐用量上限打断的一轮,在上限重置后自动接着做 | 有 | 有 | 无 | 由其自身重试 | 由其自身重试 | 无 | 无 | 无 |
| 一轮进行中转圈,结束后成环 | 有 | 有 | 有 | 有 | 有 | 无 | 无 | 有 |
| 智能体卡在等你时响铃 | 有 | 有 | 无 | 有 | 无 | 无 | 无 | 无 |
| 侧栏按状态筛选:Waiting、Running、Unread | 有 | 有 | 无 Waiting | 有 | 无 Waiting | 无,总在 Idle | 无,总在 Idle | 无 Waiting |
Expand Down
15 changes: 15 additions & 0 deletions docs/ceilings.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,21 @@ work when nobody knows it and that the call site never shows. The mechanism and
those logins and never writes them: it does not refresh the token, so an expired one reads as signed out until
the provider's own CLI rotates it. A reading is cached for five minutes because both endpoints rate-limit hard —
the number on screen is up to that old, and nothing on it says so.
- **A turn a usage limit stopped is picked back up on two providers only** (`internal/terminal/limit.go`,
`internal/relay/resume.go`): Claude Code and Codex are the two that write the limit down with its reset. On
Antigravity, Crush, Cursor CLI and Kiro CLI nothing marks a limit at all, so the card keeps spinning until the
next prompt; opencode and oh-my-pi retry by themselves and lich adds nothing on top. Claude Code's end arrives
only as `StopFailure`, which an older lich-plugin does not report. Codex raises no hook for it, so an open
Codex turn is read off its rollout every 30 seconds, and the limit is *inferred*: a turn completing with
nothing said right after a window shows 100% spent (measured on `codex exec` 0.144.5, never on the TUI); a
change to that shape stops the read without a word. The continuation is a scheduled prompt told apart from a
person's by its exact text, so someone parking those same words has them dropped on the session's next turn.
Nothing is parked for a reset already past or more than 24 hours out (Claude Code's own auto-continue gives up
at the same horizon), nor over a prompt already parked there. Any turn starting before it is due drops it,
which is also what keeps it from landing behind Claude Code's own `autoContinueAtUsageLimit`, a setting the
server enables per account, which is why the prompt is due two minutes after the reset rather than at it. The
card's limit rung lives in the window alone: a reload drops it, while the parked prompt stays on the card as a
plain scheduled one.
- **The pace marker is borrowed calibration, and it is silent far more often than it is wrong**
(`internal/quota/pace.go`): the two numbers that decide when a weekly window is marked as spending ahead —
fifteen percentage points past the elapsed share, and no marking at all in the first twenty-four hours after
Expand Down
16 changes: 15 additions & 1 deletion docs/hooks/session-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Both sides test against the payloads in
| `PreToolUse` | `PreToolUse` | `PreToolUse` | `tool.execute.before` | `tool_call` | — | dropped | `preToolUse` | `busy` + `tool` |
| `PostToolUse` | `PostToolUse` | — | `tool.execute.after` | `turn_start` | — | dropped | `postToolUse` | `busy` |
| `Notification` | `PermissionRequest` | — | any `*.asked` | — | — | — | — | `waiting` + `reason` |
| `Stop` | `Stop` | `Stop` | `session.status` (`idle`) | `session_stop` | — | — | `stop` | `done` |
| `Stop`, `StopFailure` | `Stop` | `Stop` | `session.status` (`idle`) | `session_stop` | — | — | `stop` | `done` |
| `SessionEnd` | — | — | — | — | — | `SessionEnd` | — | `idle` |

**Kiro closes four of the six rows and neither of the other two.** It has no
Expand All @@ -69,6 +69,20 @@ same payload. Its `idle` means the turn ended, which is lich's `done` — not
lich's `idle`, which says the CLI itself has left. Nothing in opencode's event
list says that, so like Codex it never reports `idle`.

**Claude Code ends a turn an API error stopped with `StopFailure`, not `Stop`.**
It fires instead of `Stop` when a usage limit, an overload or a failed login ends
the turn (2.1.293), and a client that registers only `Stop` leaves the card
spinning until the next prompt. It reports `done` like `Stop`: the turn is over
either way. Which error it was is not in the report: lich reads it off the
transcript on every `done`, and a usage limit it finds there parks the turn's
continuation for when the limit resets (docs/ceilings.md).

**Codex reports no end at all for a turn a usage limit stopped**: measured on
0.144.5, `UserPromptSubmit` fires and `Stop` never does. lich ends that turn
itself, reading the rollout of every open Codex turn, and publishes it as
`interrupted`, the state it already raises for a turn that ended without the
provider saying so.

**oh-my-pi carries `busy` on `turn_start` rather than after a tool.** Its
non-interactive runs never emit `input` at all, so the turn boundary is the one
event every turn passes through — the same place the title is re-read, since omp
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/components/render-budget.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,11 @@ async function mountSidebar() {
return budget
}

// What one card drags along when it repaints: its own chrome, and the base-ui
// context-menu and tooltip parts wrapped around it. The unnamed pair are
// base-ui's own anonymous render functions.
// What one card drags along when it repaints: its own chrome, its status line,
// and the base-ui context-menu and tooltip parts wrapped around it. The unnamed
// pair are base-ui's own anonymous render functions.
const CARD_CHROME = {
SessionStatusRung: 1,
ContextMenu: 1,
ContextMenuRoot: 1,
ContextMenuTrigger: 2,
Expand Down Expand Up @@ -285,10 +286,12 @@ test("a finished turn repaints that session's card and no other block", async ()
await budget.unmount()
})

test("a todo event repaints that session's card and nothing else", async () => {
// The task list is read by the status line alone, so its count moving repaints
// that line and leaves the card around it, and its menus and tooltip, alone.
test("a todo event repaints that session's status line and nothing else", async () => {
const budget = await mountSidebar()
await budget.act(() => bus.emit(TODO_EVENT, { id: "s2", done: 3, total: 7 }))
expect(budget.take()).toEqual({ "SessionCard#s2": 1, ...CARD_CHROME })
expect(budget.take()).toEqual({ SessionStatusRung: 1 })
await budget.unmount()
})

Expand Down
Loading
Loading