feat: auto-delete old conversations, and stop a tool call that repeats
This commit is contained in:
@@ -17,6 +17,8 @@ Hit a global shortcut, type a question, get a streamed answer without leaving wh
|
||||
- **Readable code.** Fenced blocks render in a dark panel with the language named in a header, syntax colouring, and a Copy button.
|
||||
- **Real markdown.** Tables, bulleted and numbered lists, headings, blockquotes, bold, italics and links all render natively, and the default prompt asks for the shape that fits the answer rather than flattening everything into prose.
|
||||
- **Tools.** Lam decides when to call them: current time, location context, web search, and page fetching. A turn that uses up its tool budget still ends in an answer, written from what it gathered, rather than a notice that it stopped.
|
||||
- **Tool calls you can watch and stop.** What Lam is doing right now stays on screen, including when it asks the same question twice, the round limit is a preference, Force Stop is always one keystroke away, and a call that repeats itself with identical arguments ends the turn instead of running forever.
|
||||
- **Optional auto-deletion.** Set a retention window and Lam clears out the conversations you have stopped using the next time you open a command, taking their screenshots with them and collecting any capture you never sent. Off by default, and nothing at all is read or written while it is off.
|
||||
- **Screenshots, two ways.** Snip a region into the conversation you are already in from the action panel, or run **Snip to Lam** to start a new chat about it. The launcher window gets out of the way first, so it never ends up in the shot, and the capture appears in the transcript as a thumbnail with Open Screenshot for the full image. Neither is offered unless the current model can actually see images.
|
||||
- **Chat only.** Lam answers questions. It does not touch your files or run commands. The one exception is capture, and it only ever happens because you pressed the key for it.
|
||||
|
||||
@@ -130,6 +132,8 @@ vicinae 'vicinae://launch/lam-agent/chat?fallbackText=summarise%20this'
|
||||
| **Show Tool Calls** | on | Lists the tools Lam called under each answer. |
|
||||
| **Show Reasoning** | on | Includes the model's thinking trace. Turn off to keep replies short. |
|
||||
| **Start New Chat After** | 1 hour | Idle time past which opening the command starts a fresh chat. `Never` always reopens the last one. The old chat is never deleted either way. |
|
||||
| **Delete Old Chats After** | Disabled | Permanently deletes a conversation once you have not opened it for this long, along with any screenshots it captured. Nothing is ever deleted while this is off. See [Auto-deletion](#auto-deletion). |
|
||||
| **Tool Call Limit** | 8 rounds | How many rounds of tool calls Lam may make before it has to answer with what it has. `Unlimited` removes the cap. A call that repeats itself with identical arguments ends the turn either way. |
|
||||
|
||||
## Tools
|
||||
|
||||
@@ -160,7 +164,31 @@ This extension does not use GPS. That would mean CoreLocation on macOS, which ha
|
||||
|
||||
Web search results and fetched pages can be large. Lam truncates them before they reach the model.
|
||||
|
||||
**A turn that runs out of tool calls still ends in an answer.** The loop stops after 8 rounds, and the first version of it stopped by throwing away every preamble the model had written and returning a notice about the loop: asked for the fish in every Valheim biome, it researched for five rounds and then said nothing useful at all. The budget is now spent on a *closing round* with the tools taken away and an instruction to answer from what was gathered and to name what it could not confirm. So the worst case is a partial answer that says what is missing, rather than an app-generated message about its own control flow. If the closing round produces nothing either, the last preamble is used, and only then a notice. The closing round is why the cap is 8 rather than 5: the extra round is not more searching, it is the answer.
|
||||
**A turn that runs out of tool calls still ends in an answer.** The loop stops after 8 rounds by default, and the first version of it stopped by throwing away every preamble the model had written and returning a notice about the loop: asked for the fish in every Valheim biome, it researched for five rounds and then said nothing useful at all. The budget is now spent on a *closing round* with the tools taken away and an instruction to answer from what was gathered and to name what it could not confirm. So the worst case is a partial answer that says what is missing, rather than an app-generated message about its own control flow. If the closing round produces nothing either, the last preamble is used, and only then a notice. The closing round is why the default cap is 8 rather than 5: the extra round is not more searching, it is the answer.
|
||||
|
||||
The cap is a preference rather than a constant, because how many rounds a question needs is a property of the question and of the model. `Unlimited` is there for research that genuinely needs a dozen pages.
|
||||
|
||||
**Two identical calls run, a third refuses to.** No round cap can be a complete guard, because one of the settings is `Unlimited`. What a stuck model actually does is repeat itself: the same tool, the same arguments, the same non-answer. So `agent.ts` counts the tool name together with its sorted arguments, per turn, allows the call twice (a retry after a search that failed is reasonable), and on the third occurrence does not run it. The round finishes, the loop breaks, and the closing round is reached with an instruction that names the repeated call instead of claiming a budget ran out. The refusal is recorded on the turn with `repeated: true`, so the transcript can say what happened.
|
||||
|
||||
Arguments are compared with their keys sorted, because models are not consistent about key order and a call that differs only by key order is still the same call.
|
||||
|
||||
**What Lam is doing is visible while it is doing it, even with tool calls hidden.** `Show Tool Calls` governs the finished transcript, where a list of names adds little. The live activity line is separate and always shown, because during a turn the difference between working and circling is the entire question: `_Reading a page…_` for a first call, and `_Reading a page · the same call again (2)_` once it has come round. Hiding that would leave the user watching a spinner they cannot interpret, which is the situation the repeat guard exists to fix. A call that failed, or that was refused for repeating, is also listed in the transcript even with tool calls hidden: those are the two cases where hiding the line leaves an answer the user cannot account for.
|
||||
|
||||
## Auto-deletion
|
||||
|
||||
Off by default. When it is on, Lam deletes a conversation once you have not used it for the configured period, and deletes the screenshots that belonged to it.
|
||||
|
||||
It is checked when any Lam command opens (chat, sessions, setup) and after every answer, so an expired conversation goes the next time you use Lam. The alternative is a timer, which would mean waking up to do work while nothing is open.
|
||||
|
||||
**Age is counted from the last time you used a conversation, not from when you started it.** A chat started last month and continued this morning is not an old chat, and deleting it because of its birthday would be a surprise.
|
||||
|
||||
**The conversation you are in is never deleted**, even if it is expired. It is about to be rendered, and being the one the command opens by default is the clearest possible sign that it is still in use. The practical consequence is that a conversation you keep returning to outlives the window indefinitely, which is the intended reading of "conversations you have not opened". Sweeping *after* the startup resolution rather than before is what makes this work: sweeping first would delete the very conversation on its way to the screen.
|
||||
|
||||
**A screenshot the user never sent is collected too.** Snips are written to the support directory before the chat view opens, so a capture you abandon belongs to no conversation and no amount of session deletion would ever reach it. The orphan pass reads the snip directory directly, compares it against the paths recorded on the conversation index, and deletes anything unreferenced that is more than a day old. That wait is the safety story: a snip is on disk seconds after it is taken and stays unreferenced until you send it, so without a grace period the sweep would delete the file out from under a question you were still typing. Files belonging to a conversation that has just been deleted go immediately with it instead of waiting, because the conversation they belonged to is already gone.
|
||||
|
||||
The index entry for a conversation carries its image paths (`SessionSummary.attachmentPaths`), so deciding what is still referenced does not mean reading every transcript, which the sweep would otherwise do after every single answer. An index written before that field existed is repaired from its transcripts before anything is deleted, because a missing list is not the same as an empty one: reading it as empty would mark every screenshot in your history as an orphan.
|
||||
|
||||
**The whole design leans towards deleting nothing.** Retention off means nothing is read and nothing is written. An unrecognised value for the preference resolves to off. The orphan pass is skipped entirely when the referenced set cannot be established, because an incomplete set of references looks exactly like a pile of orphans and guessing wrong destroys real images. A path is checked to be inside the snip directory before anything is removed, so a hand-edited entry in storage cannot reach anything else on the disk. And the sweep never throws, so a failed tidy-up cannot break the command you opened.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -178,6 +206,8 @@ src/
|
||||
attachments.ts # image files -> base64 payloads
|
||||
vision.ts # cached "can this model see?" answer
|
||||
sessions.ts # LocalStorage persistence
|
||||
retention.ts # what gets deleted, and when
|
||||
cleanup.ts # the sweep + its notification
|
||||
history.ts # turns -> API messages
|
||||
location.ts # time/place, derived locally
|
||||
thread.ts # turns -> flat message list
|
||||
@@ -224,6 +254,8 @@ The rendered height stays capped at 200px whatever is asked for (`Math.min(_rawH
|
||||
|
||||
**Streamed deltas are buffered in refs and flushed to state on a timer** (`FLUSH_INTERVAL_MS`). Re-rendering the transcript per token makes the launcher visibly janky.
|
||||
|
||||
**Stopping a turn is an action, not a keybinding.** The stop action is labelled Force Stop and stays the primary action for the whole turn, because the failure it exists for is a model spending a long time on a question you have given up on. It aborts the request, which propagates through the tool loop and into the tool that is running, so it works mid-search rather than only between rounds. There is no separate stop command: an extension cannot reach into a view running in another command, so a stop command would have to poll a flag in `LocalStorage` and could only stop at round boundaries, which is both slower and less reliable than the thing it would replace.
|
||||
|
||||
**The logic layer has no Vicinae imports.** `agent.ts`, `ollama.ts`, `tools.ts` and friends are plain TypeScript, which is why they unit test without a runtime. A test enforces this.
|
||||
|
||||
**Sessions are capped twice over**, once per turn and once per session, because `LocalStorage` has no documented ceiling.
|
||||
|
||||
Reference in New Issue
Block a user