From 58b96273337d1a17520f0a1c8f16640c3f2ecf93 Mon Sep 17 00:00:00 2001
From: jaymar921
Date: Fri, 14 Aug 2026 20:13:55 +0800
Subject: [PATCH 1/5] feat(0.8.0): stop a compliment from restarting finished
work
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The last message of the 0.7.0 `qwen3.5:4b` evaluation session was "It all works
now, thank you". The agent answered it by building a checklist and starting to
re-fix bugs it had already fixed — items carried over from two turns earlier.
The user cancelled the run.
Traced through `classify`, the message matches nothing: no mutating verb, no work
verb, no file, and `isPurelySocial` rejects it because `works` and `now` are not
social words. It reached the `task` default.
The gap is a category, not two words. Adding `works` to the social vocabulary
would be wrong — "the delete button no longer works" is a bug report. So a
success report is matched as a phrase, and any sign the sentence goes on to say
something is still wrong (`but`, `still`, a negation, `almost`) hands it straight
back to the agent. It is checked after the mutating-verb rule, so "it works now,
can you also add a dark mode" stays a task.
Fixing that surfaced a live bug in `isGreetingWithName`, which tested the first
word against the whole of SOCIAL_WORDS. That set is mostly filler — "it", "the",
"got", "all" — admitted there on the strength of a rule that only holds for whole
messages. Read one word at a time it made any message of three words or fewer a
greeting, so on main today:
"it doesn't work" -> chat
"the tests fail" -> chat
"got an error" -> chat
"all buttons broken" -> chat
Four bug reports answered conversationally with the request dropped, which is the
one outcome this module's header says it must never produce. Greetings now match
against a dedicated GREETING_WORDS set.
The new pattern is flagged `detect-unsafe-regex`; it was measured rather than
argued. Every optional group carries a distinct literal prefix, so none can claim
the same characters — 60,000-character adversarial inputs run in 0.5ms, flat.
Also adds doc/SESSION-ANALYSIS-0.7.0.md, the counted analysis of both evaluation
sessions that the rest of 0.8.0 follows from.
---
app/core/intentRouter.js | 123 ++++++++++++++++-
doc/SESSION-ANALYSIS-0.7.0.md | 241 +++++++++++++++++++++++++++++++++
test/unit/intentRouter.test.js | 56 ++++++++
3 files changed, 419 insertions(+), 1 deletion(-)
create mode 100644 doc/SESSION-ANALYSIS-0.7.0.md
diff --git a/app/core/intentRouter.js b/app/core/intentRouter.js
index 6e46b69..bca4aca 100644
--- a/app/core/intentRouter.js
+++ b/app/core/intentRouter.js
@@ -401,6 +401,83 @@ const ABOUT_THE_CONVERSATION =
const ABOUT_THE_PROGRESS =
/\bwhere\s+are\s+we\b|\bwhat(?:'s| is)?\s+the\s+(?:state|status|progress)\b|\bwhat\s+have\s+we\s+(?:done|got|finished)\b|\bcatch\s+me\s+up\b|\brecap\b|\bwhere\s+did\s+we\s+(?:leave|stop|get)\b/i;
+/**
+ * The user telling you the work is finished and correct.
+ *
+ * ## The failure this exists for
+ *
+ * The last message of the `qwen3.5:4b` evaluation session was
+ *
+ * It all works now, thank you
+ *
+ * and the agent answered it by building a checklist and starting to re-fix bugs it had
+ * already fixed — `1. Fix the onToggleComplete function error in TodoItem.jsx`, an item
+ * carried over from two turns earlier. The user cancelled it. A thank-you did not merely
+ * start a run; it started a run that was about to undo finished work.
+ *
+ * Traced through `classify`, the message matches nothing: no mutating verb, no work
+ * verb, no file, and `isPurelySocial` rejects it because `works` and `now` are not in
+ * `SOCIAL_WORDS`. It reached the `task` default.
+ *
+ * ## Why this is a category and not two more words
+ *
+ * Adding `works` and `now` to `SOCIAL_WORDS` would be actively wrong: **"the delete
+ * button no longer works"** is a bug report and has to stay a task. What separates the
+ * two is not vocabulary, it is who the sentence says the subject is and whether it is
+ * negated — so this is matched as a phrase, and any hint that something is still wrong
+ * hands the message straight back to the agent.
+ *
+ * ## What is deliberately not matched
+ *
+ * "no more errors" and "nothing is broken" are success reports too, and both are left
+ * out. They are built from the same words as the complaints they invert, and this
+ * module's standing trade — being wrong toward `task` costs one loop, being wrong
+ * toward `chat` drops a request — says to miss them rather than risk the inverse.
+ *
+ * ## On the `detect-unsafe-regex` warning
+ *
+ * The linter flags the run of optional `\s+`-terminated groups in the first branch. It
+ * is a false positive, and it was measured rather than argued: every optional group
+ * carries a distinct literal prefix (`all`, `is`/`are`, `seems`/`appears`, `now`), so no
+ * two can ever claim the same characters and there is nothing to backtrack over. On
+ * 60,000-character adversarial inputs — long whitespace runs, and `"all "`, `"seems to "`
+ * and `"now "` repeated to fill the buffer — the worst case is 0.24 ms, flat in the
+ * length.
+ *
+ * The branches, in order: "it works" / "it all works now" / "everything is running";
+ * "that fixed it"; "working now" / "works perfectly"; "all good"; "we're all set";
+ * "looks good now".
+ */
+const SUCCESS_REPORT =
+ /\b(?:it|that|this|they|everything)\s+(?:all\s+)?(?:is\s+|are\s+)?(?:seems?\s+to\s+|appears?\s+to\s+)?(?:now\s+)?(?:works?|worked|working|runs?|ran|running)\b|\bthat\s+(?:fixed|did|solved|sorted)\s+it\b|\b(?:works?|working|running)\s+(?:now|perfectly|fine|great|well)\b|\ball\s+(?:good|set|sorted|working|fixed)\b|\bwe(?:'re|\s+are)\s+(?:good|set|done|all\s+set)\b|\b(?:looks?|seems?)\s+(?:good|fine|great|right)\s+now\b/i;
+
+/**
+ * Any sign the sentence goes on to say something is still wrong.
+ *
+ * "it works" and "it doesn't work" differ by one word; so do "that fixed it" and "that
+ * didn't fix it". A contrast word is the other half — "it works **but** the clear button
+ * doesn't" opens with a success report and is a bug report.
+ *
+ * Deliberately broad, because every word in here costs at most one agent loop on a
+ * message that was in fact a compliment, and buys back the case where a report of
+ * partial success would otherwise have been dropped on the floor.
+ */
+const STILL_WRONG =
+ /\b(?:isn'?t|aren'?t|wasn'?t|doesn'?t|don'?t|didn'?t|won'?t|can'?t|cannot|couldn'?t|shouldn'?t|not|no|never|still|except|unless|but|however|though|although|why|almost|nearly|mostly|partly)\b/i;
+
+/**
+ * Is this the user reporting the work now succeeds, and nothing more?
+ *
+ * @param {string} text
+ * @returns {boolean}
+ */
+function reportsSuccess(text) {
+ const message = String(text || '').trim();
+ if (message.length === 0) return false;
+ if (!SUCCESS_REPORT.test(message)) return false;
+ return !STILL_WRONG.test(message);
+}
+
/**
* Is every word in this message a social one?
*
@@ -425,6 +502,37 @@ function isPurelySocial(message) {
/** Words a greeting may be followed by without becoming a request. */
const GREETING_TAIL_WORDS = 3;
+/**
+ * The words that actually open a greeting.
+ *
+ * A strict subset of `SOCIAL_WORDS`, and the distinction cost real requests before it
+ * existed. `isGreetingWithName` used to ask whether the first word was anywhere in
+ * `SOCIAL_WORDS` — but that set is mostly *filler* ("a", "lot", "the", "it", "no",
+ * "got", "all", "fine"), admitted there on the strength of the rule that a message only
+ * counts as social when **every** word is in it. Read one word at a time, it means any
+ * message of three words or fewer beginning with a common English word is a greeting:
+ *
+ * "it doesn't work" → chat
+ * "the tests fail" → chat
+ * "got an error" → chat
+ * "all buttons broken" → chat
+ *
+ * Four bug reports, answered conversationally, request dropped. Which is the precise
+ * failure this module's header says it must never produce, arriving through the one
+ * rule that reads the vocabulary word-by-word instead of whole-message.
+ *
+ * The compound openers — `good`, `magandang`, `buenos`, `guten` — are kept, because
+ * "good morning gemma4" is the case the rule was written for. They are safe here in a
+ * way they are not in the filler set: each begins a real greeting, and a message like
+ * "good work thanks" that they do claim is a compliment, which belongs in chat anyway.
+ */
+const GREETING_WORDS = new Set([
+ 'hi', 'hey', 'hello', 'yo', 'sup', 'hiya', 'howdy', 'greetings',
+ 'kumusta', 'kamusta', 'musta', 'mabuhay', 'magandang',
+ 'hola', 'buenos', 'buenas', 'bonjour', 'salut', 'ciao', 'hallo', 'guten',
+ 'good',
+]);
+
/**
* A greeting with a name on it.
*
@@ -447,7 +555,7 @@ function isGreetingWithName(message) {
.filter(Boolean);
if (words.length === 0 || words.length > GREETING_TAIL_WORDS) return false;
- return SOCIAL_WORDS.has(words[0]);
+ return GREETING_WORDS.has(words[0]);
}
/**
@@ -474,6 +582,15 @@ function classify(text) {
return { intent: 'task', reason: 'asks for a change' };
}
+ // After the mutating-verb rule rather than before it, which costs "it works now,
+ // thanks for fixing it" a wasted loop — `fixing` claims it first. That is the correct
+ // way round: "it works now, can you also add a dark mode" is the same shape, and
+ // answering *that* conversationally would drop a request. A change asked for always
+ // wins, even when it arrives wrapped in a compliment.
+ if (reportsSuccess(message)) {
+ return { intent: 'chat', reason: 'reports the work now succeeds' };
+ }
+
// Checked ahead of the broader work-verb rule, and only now that a mutating request
// has been ruled out. These three are about the assistant and the conversation, and
// they routinely contain a word from `WORK_VERB` while asking for no work at all:
@@ -545,11 +662,15 @@ module.exports = {
ASKS_FOR_BANTER,
isPurelySocial,
isGreetingWithName,
+ reportsSuccess,
+ SUCCESS_REPORT,
+ STILL_WRONG,
WORK_VERB,
MUTATING_VERB,
PLANNED_DELIVERABLE_VERB,
NAMES_A_FILE,
SOCIAL_WORDS,
+ GREETING_WORDS,
ASSENT_WORDS,
ABOUT_THE_ASSISTANT,
ABOUT_THE_CONVERSATION,
diff --git a/doc/SESSION-ANALYSIS-0.7.0.md b/doc/SESSION-ANALYSIS-0.7.0.md
new file mode 100644
index 0000000..91cc591
--- /dev/null
+++ b/doc/SESSION-ANALYSIS-0.7.0.md
@@ -0,0 +1,241 @@
+# What the 0.7.0 live sessions showed
+
+Two models were given the same brief on Machine B — build a React + Vite + Tailwind TODO
+app from scratch, to a fixed folder structure — and run to whatever end they reached.
+`qwen3.5:4b` finished it. `qwen3.5:0.8b` never wrote a single file.
+
+The logs are in `.ignore/1.todo-app-0.7.0-qwen3.5-4b` and
+`.ignore/2.todo-app-0.7.0-qwen3.5-0.8b`: `outcomes.jsonl` for the step and session
+ledger, `audit.log` for every tool call with its path, `transcripts/session1.json` for
+what the user actually saw.
+
+This document is the evidence behind the 0.8.0 work items. Everything below is counted
+from those files rather than remembered from watching the runs.
+
+---
+
+## The headline numbers
+
+| | `qwen3.5:4b` (Tier A) | `qwen3.5:0.8b` (Tier B) |
+|---|---|---|
+| Sessions | 11 | 7 |
+| Steps taken | 126 | 22 |
+| `write_file` calls | 21 | **0** |
+| Wall clock | 88.2 min | 1.9 min |
+| Share of wall clock spent in inference | **97%** | 99% |
+| Sessions ending `repeating` | 0 | **5 of 7** |
+| Task completed | yes | no |
+
+The two failures are different in kind, and neither is a failure of the model's coding
+ability. The 4B model wrote correct code slowly. The 0.8B model was stopped before it
+was ever allowed to write any.
+
+---
+
+## 1. The 0.8B model: killed by the repeat guard, every time
+
+Five of seven sessions ended with `stopReason: "repeating"`. Four of those five ended
+at **exactly two steps**. The action ledger for the whole evaluation:
+
+```
+list_files 12 (55% of all steps)
+run_script 7
+read_file 3
+write_file 0
+```
+
+The shape is the same every session. The model calls `list_files`, gets the listing,
+calls `list_files` again with the same arguments, gets the same listing, calls it a
+third time — and `reactLoop`'s repeat guard (`REPEAT_LIMIT = 2`) ends the entire
+session. The user saw this seven times:
+
+> I stopped because I kept repeating the same step (list_files). Before that I completed
+> 2 step(s) — check the changes above before relying on them.
+
+### Why the existing anti-repetition machinery did not catch it
+
+It did fire. `nextStepHint` has a case for exactly this
+([reactLoop.js:230](../app/agent/reactLoop.js#L230)):
+
+> You now know what is in the project. Open the file you need with `read_file`.
+
+and a stronger one on the second occurrence ([reactLoop.js:216](../app/agent/reactLoop.js#L216)):
+
+> You have already done list_files and have the result above. Do NOT do it again.
+
+The model was told, in plain English, twice, and did it again anyway. That is the
+finding. **At 0.8B, a hint is not a control.** Everything in the loop that prevents
+repetition is currently written as text addressed to the model's judgement, and this
+model has no budget for judgement — it has 2,000 prompt tokens, no plan, no checklist,
+and a fresh context every turn.
+
+### And the punishment is aimed at the wrong thing
+
+Repeating `list_files` is not a dangerous act. It is a read-only call on a directory,
+costing 5 milliseconds of tool time. The response to it is to **end the user's whole
+session** — the most destructive outcome available — while a genuinely expensive
+mistake (a wrong `npm install`) gets a diagnosis and a retry.
+
+The guard was designed against a model burning its budget in a loop. Against a model
+that is merely disoriented, it converts confusion into termination.
+
+### Compounding: this tier gets none of the scaffolding
+
+At 0.87B the model falls below two thresholds at once
+([modelCapability.js:211](../app/core/modelCapability.js#L211),
+[modelCapability.js:244](../app/core/modelCapability.js#L244)):
+
+- `canPlanTodos` requires ≥ 2B → **no checklist, no per-item `stepBrief`**
+- Tier B budgets set `planning: 'none'` → **no plan either**
+
+So the only structure this model ever receives is one `goalReminder` line and one
+`nextStepHint` line appended to a 2,000-token prompt. `stepBrief` — the module whose
+entire job is to tell a model what it already did, what exists, and what to do next —
+is unreachable from this tier. It is built and tested and the model that needs it most
+never sees it.
+
+---
+
+## 2. The 4B model: correct, and slow for a reason that is not the GPU
+
+88.2 minutes of wall clock, of which **85.7 minutes were inference** — 97%. Tool
+execution across all 126 steps totalled about 152 seconds, and 150 of those were
+`run_script` (npm doing real work). The remaining six tool types cost **1.8 seconds
+combined**. All 73 `read_file` steps together took 1.2 seconds.
+
+So the run was not slow because of disk, or the extension, or the GPU. It was slow
+because it took **126 model round-trips at roughly 42 seconds each**, and most of those
+round-trips did nothing but move a file the model had already seen back into its context.
+
+### 90% of reads were of a file the agent had already read
+
+From `audit.log`: **263 `read_file` entries across 25 distinct paths.**
+
+| Times read | Path |
+|---|---|
+| 57 | `.` |
+| 28 | `todo-glass-app/src/App.jsx` |
+| 17 | `todo-glass-app/src/components/ClearButton.jsx` |
+| 16 | `todo-glass-app/src/components/TodoInput.jsx` |
+| 15 | `todo-glass-app/src/hooks/useTodos.js` |
+| 15 | `todo-glass-app/src/components/TodoList.jsx` |
+| 14 | `todo-glass-app/src/App.css` |
+| 13 | `todo-glass-app/src/assets/hero.png` |
+| 13 | `todo-glass-app/src/assets/react.svg` |
+| 13 | `todo-glass-app/src/assets/vite.svg` |
+| 13 | `todo-glass-app/src/components/TodoItem.jsx` |
+| 12 | `todo-glass-app/src/components/TodoStats.jsx` |
+
+`App.jsx` was read 28 times and written 4 times. A binary PNG was read into the prompt
+13 times. At the step level, **73 of 126 steps (58%) were `read_file`**, against 21
+writes.
+
+Nothing in the loop tracks that a file has already been read. `nextStepHint` says "do
+not read it again" after a successful read, and — as with the 0.8B case — that is a
+sentence, not a mechanism. There is no cache, no per-session file table, and no way for
+the agent to be handed content it already has without spending a step to ask for it.
+
+### What that costs, bounded honestly
+
+The audit figure (263 reads / 25 paths) includes reads the extension performs itself
+while building context, so it is not a clean count of wasted *steps*. The clean figure
+is the step ledger: 73 `read_file` steps for 25 distinct files. Even assuming every file
+legitimately needed re-reading once after each of the 21 writes, that leaves roughly
+**25–30 steps of pure redundancy — 18–24 minutes of this session's 88.**
+
+---
+
+## 3. Casual messages start real work
+
+Two instances, one per model, and they have the same root cause.
+
+**4B, final exchange.** The user wrote:
+
+> It all works now, thank you
+
+The agent built a checklist and began re-fixing bugs it had already fixed:
+
+> 1. Fix the `onToggleComplete` function error in TodoItem.jsx. — not completed (stopped: error)
+> 2. Resolve the issue where the Clear all button does not clear the todo list. — not attempted (the session was cancelled)
+
+The user aborted it. Note what the checklist contains: the *previous* turn's items,
+regenerated. A thank-you did not merely start a run — it started a run that would have
+undone finished work.
+
+**0.8B, second message.** The user wrote `hi`. The model replied with a complete
+`App.jsx` in a code fence — 1,900 characters of inline-styled React, no Tailwind, no
+components, contradicting the folder structure it had been given. Nothing was written to
+disk (`changed: false`), so the user was shown a plausible finished app that did not
+exist anywhere.
+
+### Why `intentRouter` let the compliment through
+
+Tracing `"It all works now, thank you"` through
+[classify()](../app/core/intentRouter.js#L465):
+
+- `MUTATING_VERB` — no match
+- `ABOUT_THE_PROGRESS`, `ABOUT_THE_CONVERSATION`, `ABOUT_THE_ASSISTANT` — no match
+- `WORK_VERB` — no match
+- `NAMES_A_FILE` — no match
+- `isPurelySocial` — **fails**: `it`, `all`, `thank`, `you` are in `SOCIAL_WORDS`, but
+ `works` and `now` are not
+- `isGreetingWithName` — fails, six words
+- falls through to the default: **`task`, "no conversational signal"**
+
+The module is not missing two words. It is missing a *category*: **the user reporting
+that the work succeeded.** "it works now", "that fixed it", "all good", "we're done" —
+none are greetings, none contain a verb, none name a file. And the fix genuinely cannot
+be to add `works` to the social vocabulary, because "the delete button no longer works"
+is a bug report and must stay a task.
+
+`hi` is a different bug with the same consequence: it classified correctly as `chat`,
+but the conversational path had the whole coding task in its context and answered by
+writing the app in prose.
+
+---
+
+## 4. What worked, and should not be disturbed
+
+Worth recording, because three of these are load-bearing and easy to break while fixing
+the above.
+
+- **Error diagnosis earned its place.** `NO_PACKAGE_JSON` and
+ `DEPENDENCIES_NOT_INSTALLED` both fired with the specific remedy attached ("Run
+ `npm install` with `"cwd": "todo-glass-app"` first"). The 4B model acted on them
+ correctly.
+- **`cwd` in the repeat key was right.** `npm install` at the root and inside
+ `todo-glass-app` are correctly distinct actions; the 4B session depended on that.
+- **The user's own error paste is the highest-value input in the run.** Every one of the
+ 4B session's real fixes came from the user pasting a console error. The agent resolved
+ the postcss plugin move, the missing autoprefixer, four missing default exports, and
+ two prop-name mismatches — each within one or two steps of being shown the message.
+- **`clarification` never blocked a session.** No spurious questions in 18 sessions.
+
+And one thing that worked and *should* be disturbed: at message 14 the user had to say
+
+> you have not modified the components, you just read them. Can you fix the exports of each components?
+
+The agent had announced the fix ("I need to add `export default` statements to the
+remaining 4 components") and then stopped, having only read them. `completionCheck`
+covers "claimed done with an empty change set", but not "announced an intention and
+ended the turn". That is a third failure mode and it is adjacent to the read-loop
+problem: the model spent its step budget reading and had none left to write.
+
+---
+
+## What this implies for 0.8.0
+
+In priority order, with the evidence each rests on:
+
+1. **Serve repeated reads from a session file table instead of a model round-trip.**
+ 58% of steps, 90% redundancy, 97% inference-bound. This is the single largest cost in
+ the log and it does not need the model's cooperation to fix.
+2. **Make anti-repetition a mechanism rather than a sentence,** and stop ending sessions
+ over read-only recon. 5 of 7 sessions on the small model died here at step 2.
+3. **Give Tier B the scaffolding it is currently excluded from** — the reminder layer of
+ `stepBrief`, restated every turn, without requiring a 2B checklist.
+4. **Add an outcome-report category to `intentRouter`,** so "it works now, thanks" ends a
+ task rather than starting one.
+5. **Show the user what the agent is doing while it does it** — at 42 seconds a step,
+ the panel is silent for minutes at a time, and the user's only signal that a run went
+ wrong is the summary at the end.
diff --git a/test/unit/intentRouter.test.js b/test/unit/intentRouter.test.js
index 7564f42..dc1468e 100644
--- a/test/unit/intentRouter.test.js
+++ b/test/unit/intentRouter.test.js
@@ -110,11 +110,67 @@ describe('intentRouter.classify', () => {
}
});
+ it('answers a report that the work now succeeds instead of restarting it', () => {
+ // Verbatim from the last turn of the 0.7.0 `qwen3.5:4b` session. The agent
+ // answered it by building a checklist and starting to re-fix bugs it had already
+ // fixed, carried over from two turns earlier. The user cancelled the run.
+ assert.strictEqual(intentOf('It all works now, thank you'), 'chat');
+
+ for (const text of [
+ 'it works now',
+ 'it works',
+ "it's working now",
+ 'that fixed it',
+ 'everything is running',
+ 'looks good now',
+ "we're all set",
+ 'works perfectly',
+ ]) {
+ assert.strictEqual(intentOf(text), 'chat', `"${text}" was treated as work`);
+ }
+ });
+
+ it('does not read a complaint as a success report', () => {
+ // The pair this rule has to keep apart. Every one of these is a bug report built
+ // from the same words as the compliment above, and treating any of them as small
+ // talk would drop the request silently — the one outcome this module forbids.
+ for (const text of [
+ 'the delete button no longer works',
+ "it doesn't work",
+ "it still doesn't work",
+ 'it works but the clear button does nothing',
+ 'it works, however the stats are wrong',
+ 'why does it work only on the first todo',
+ 'it almost works',
+ 'it mostly works now',
+ ]) {
+ assert.strictEqual(intentOf(text), 'task', `"${text}" was treated as conversation`);
+ }
+ });
+
+ it('lets a change asked for beside a compliment win', () => {
+ // A compliment is not a reason to stop reading the rest of the sentence.
+ assert.strictEqual(intentOf('it works now, can you also add a dark mode'), 'task');
+ assert.strictEqual(intentOf('that fixed it — now delete the old file'), 'task');
+ });
+
it('answers a greeting that has a name on it', () => {
// "gemma4" is in no vocabulary and never could be — it is whatever the user has
// installed. The model replied by asking for "the full task description".
assert.strictEqual(intentOf('hello gemma4'), 'chat');
assert.strictEqual(intentOf('hi claude'), 'chat');
+ assert.strictEqual(intentOf('good morning gemma4'), 'chat');
+ });
+
+ it('does not read a short bug report as a greeting with a name', () => {
+ // The rule tested the first word against the whole of SOCIAL_WORDS, which is
+ // mostly filler — "it", "the", "got", "all" are all in there, admitted on the
+ // strength of a whole-message rule. Read one word at a time it made any message
+ // of three words or fewer a greeting, and every one of these was answered
+ // conversationally with the request dropped.
+ for (const text of ['it doesn\'t work', 'the tests fail', 'got an error', 'all buttons broken']) {
+ assert.strictEqual(intentOf(text), 'task', `"${text}" was treated as a greeting`);
+ }
});
it('answers a question about where the work has got to', () => {
From 8245767b5594693cb752651a6678038b32498487 Mon Sep 17 00:00:00 2001
From: jaymar921
Date: Fri, 14 Aug 2026 20:22:03 +0800
Subject: [PATCH 2/5] feat(0.8.0): tell the model what it already has, and stop
killing runs over a listing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both 0.7.0 evaluation sessions lost most of their value to the same missing
thing, at opposite ends of the model range.
`qwen3.5:0.8b` never wrote a single file. Five of its seven sessions ended
`repeating`, four at exactly two steps: list_files, list_files, list_files, run
over. Twelve of its twenty-two total steps were list_files.
`qwen3.5:4b` finished and took 88 minutes, 97% of it inside the model. 73 of its
126 steps were read_file against 21 writes; the audit log has 263 read entries
across 25 distinct paths. App.jsx was read 28 times and written 4. A binary PNG
was read into the prompt 13 times.
The existing hints fired correctly every time — "You now know what is in the
project", "Do NOT do it again" — and both models did it again anyway. That is the
finding: every anti-repetition device in the loop is a sentence addressed to the
model's judgement, evaluated against a context that no longer contains the thing
it describes. The model is asked to take the loop's word for what it already has,
and reaching for the tool is the cheaper way to be sure.
So `agent/workingSet` keeps the record instead of asserting it — paths read,
written, listed, deleted, commands run, and what last went wrong — and renders it
back as a standing block. It runs off the step trace, so a 0.8B model gets the
same footing `stepBrief` gives a 4B one without needing the 2B checklist
threshold it is excluded from.
Tier B additionally stops treating a repeated read-only action as fatal. A
repeated list_files is five milliseconds of directory read, and the response was
to end the user's whole run, while a wrong `npm install` gets a diagnosis and
another go. It now gets one substitution: the result it already had, handed back
with an instruction naming the next move. Repeat again and the guard ends the run
as before. Mutating and executing actions are untouched — repeating run_script
can install packages and start servers.
On Tier A the block is advisory and is moved rather than appended, so one copy
exists and it is always the current one, always last.
One existing test changed: Plan mode's "no checklist from the loop" case scripted
exactly three identical reads to reach the guard. The third is now answered, so
it takes a fourth to arrive at the same stop the test is about.
---
app/agent/nativeToolLoop.js | 37 ++++
app/agent/reactLoop.js | 70 ++++++++
app/agent/workingSet.js | 301 +++++++++++++++++++++++++++++++++
test/unit/agentSession.test.js | 5 +
test/unit/workingSet.test.js | 271 +++++++++++++++++++++++++++++
5 files changed, 684 insertions(+)
create mode 100644 app/agent/workingSet.js
create mode 100644 test/unit/workingSet.test.js
diff --git a/app/agent/nativeToolLoop.js b/app/agent/nativeToolLoop.js
index a149281..3f59c37 100644
--- a/app/agent/nativeToolLoop.js
+++ b/app/agent/nativeToolLoop.js
@@ -25,6 +25,7 @@
const logger = require('../utils/logger');
const { parseToolCalls, REQUIRED_FIELDS } = require('../core/outputParser');
const { truncateToTokens } = require('../utils/tokenBudget');
+const { WorkingSet } = require('./workingSet');
/**
* Required arguments a native tool call arrived without.
@@ -145,6 +146,11 @@ async function run(options) {
/** @type {Map} */
const seen = new Map();
+ /** What the agent is holding — see `agent/workingSet`. */
+ const workingSet = new WorkingSet();
+ /** The single working-set message in `messages`, moved to the end each turn. */
+ let heldMessage = /** @type {{role: string, content: string} | null} */ (null);
+
let summary = '';
let stopReason = 'budget';
let narratedCalls = 0;
@@ -309,6 +315,7 @@ async function run(options) {
emit({ type: 'action', step: steps.length + 1, action });
const result = await execute(action);
steps.push({ action, result });
+ workingSet.record(action, result, steps.length);
emit({ type: 'observation', step: steps.length, action, result });
messages.push({
@@ -321,6 +328,36 @@ async function run(options) {
}
if (stopped) break;
+
+ // What the agent is holding, restated after the tool results and before the next
+ // decision.
+ //
+ // Tier A does not lose file contents the way Tier B does — the whole exchange stays
+ // in `messages` — and it re-read anyway. On the 0.7.0 benchmark `qwen3.5:4b` spent
+ // 73 of 126 steps on `read_file` for 25 distinct paths; `App.jsx` was read 28 times
+ // and written 4, and a binary PNG was read 13 times. 97% of the 88-minute run was
+ // inference, so each of those redundant turns cost roughly 42 seconds of the user's
+ // afternoon and told the model nothing it did not already have.
+ //
+ // A long transcript is not the same as an accessible one: by turn forty the first
+ // read of `App.jsx` is thousands of tokens back and competing with everything since.
+ // This is one short list, adjacent to the decision, saying which paths are already
+ // in hand. Unlike Tier B's version it is advisory — nothing here refuses a call —
+ // because a Tier A model re-reading after a write it did not make is sometimes right.
+ // Moved rather than appended. Pushing a fresh block each turn would leave forty
+ // stale copies in the transcript by the end of a long run — each one a list of files
+ // that was accurate when written and is now contradicted by the next copy down. The
+ // previous block is spliced out so exactly one exists, always the current one, and
+ // always last.
+ const held = workingSet.render({ includeStruggles: true });
+ if (held) {
+ if (heldMessage) {
+ const at = messages.indexOf(heldMessage);
+ if (at !== -1) messages.splice(at, 1);
+ }
+ heldMessage = { role: 'user', content: held };
+ messages.push(heldMessage);
+ }
}
if (!summary) {
diff --git a/app/agent/reactLoop.js b/app/agent/reactLoop.js
index 3c9f420..c3b1e0e 100644
--- a/app/agent/reactLoop.js
+++ b/app/agent/reactLoop.js
@@ -31,10 +31,22 @@
const logger = require('../utils/logger');
const { parseAction, actionSchema } = require('../core/outputParser');
const { truncateToTokens } = require('../utils/tokenBudget');
+const { WorkingSet, isRecon } = require('./workingSet');
/** How many identical actions before the loop intervenes. */
const REPEAT_LIMIT = 2;
+/**
+ * How many times a repeated read-only action is answered rather than fatal.
+ *
+ * One. The substitution's whole claim is that the model repeated itself because it had
+ * lost the result, so handing the result back should settle it. A model that repeats the
+ * same recon action *again*, with the content and an explicit instruction both in front
+ * of it, is not disoriented — it is stuck, and the honest end to that run is the stop
+ * the guard was already going to produce.
+ */
+const RECON_SUBSTITUTION_LIMIT = 1;
+
/** How many consecutive unparseable turns before giving up. */
const PARSE_FAILURE_LIMIT = 3;
@@ -368,11 +380,15 @@ async function run(options) {
/** Status sentences this loop has shown the model, for the echo check below. */
/** @type {Set} */
const notices = new Set();
+ /** What the agent is holding, rendered into every turn — see `agent/workingSet`. */
+ const workingSet = new WorkingSet();
let observation = '';
let summary = '';
let stopReason = 'budget';
let parseFailures = 0;
+ /** Recon repeats answered with their own result rather than a stop. */
+ let substitutions = 0;
/** A `done` has already been sent back once for want of evidence. */
let doneChallenged = false;
// Two independent nudges. `hint` is about the task ("you have the file, now edit
@@ -424,6 +440,11 @@ async function run(options) {
const sections = [
options.context,
renderTrace(steps, traceBudget),
+ // The trace above says which actions ran; this says what the agent is *holding*
+ // as a result. They read similarly and do different jobs — a trace line reading
+ // "3. read_file src/App.jsx → ok" is a history entry, and a model that has lost
+ // the file itself answers it by reading the file again. See `agent/workingSet`.
+ workingSet.render({ includeStruggles: budgets.promptTokenTarget >= 1800 }),
observation ? `Result of your last action:\n${observation}` : '',
hint,
parseNudge,
@@ -539,6 +560,54 @@ async function run(options) {
const repeats = (seen.get(key) || 0) + 1;
seen.set(key, repeats);
+ // A repeated *reconnaissance* action does not end the session on the first strike.
+ //
+ // This is the single most expensive rule of the 0.7.0 round. Five of `qwen3.5:0.8b`'s
+ // seven sessions ended here, four of them at exactly two steps: list_files,
+ // list_files, list_files, session over, nothing written, seven times in a row. The
+ // model was not burning a budget or damaging anything — it was listing a directory,
+ // a read-only call costing five milliseconds, and the response was to end the user's
+ // whole run. Meanwhile a genuinely costly mistake, a wrong `npm install`, gets a
+ // diagnosis and another go.
+ //
+ // So a recon repeat gets one intervention first: the result it already had, handed
+ // back with the working set and an instruction naming the next move. It costs one
+ // turn, and it is the turn in which the model has both the content and a statement
+ // that it has the content. If it repeats *again* after that, the guard falls through
+ // and ends the run as before — a model ignoring the substitution twice is genuinely
+ // stuck, and the rung above this one is `errorRecovery` asking the user.
+ //
+ // Mutating and executing actions are untouched: repeating `run_script` can install
+ // packages and start servers, and "it was only a repeat" is no comfort there.
+ if (repeats > REPEAT_LIMIT && isRecon(action.action) && substitutions < RECON_SUBSTITUTION_LIMIT) {
+ substitutions += 1;
+ // Not charged against the repeat budget, so the model is not immediately over the
+ // line again on its next turn — but `substitutions` is capped, so this cannot
+ // become a way to loop forever.
+ seen.set(key, repeats - 1);
+ logger.info(`Substituting for a repeated recon action "${key}" rather than ending the session.`);
+
+ const previous = steps.find(
+ (entry) => entry.action && actionKey(entry.action) === key && entry.result && entry.result.ok
+ );
+ hint =
+ `STOP. You have already done ${action.action}${action.path ? ` on ${action.path}` : ''} and the result is ` +
+ 'above — asking for it again returns the same thing and gets you no further. ' +
+ (activeRoute.allowedActions.has('write_file')
+ ? 'Your next action must change a file: send write_file with "path" and the COMPLETE file contents in "code". ' +
+ 'If you genuinely cannot write anything yet, reply "done" and say what is blocking you.'
+ : 'Look at a different file, or reply "done" with what you have found.');
+ // The content, not just the assertion. The whole reason a hint alone failed is
+ // that it described something the model could no longer see.
+ if (previous && previous.result) {
+ observation = truncateToTokens(previous.result.observation, Math.floor(budgets.promptTokenTarget * 0.45), {
+ keep: 'both',
+ }).text;
+ }
+ emit({ type: 'repeat-substituted', action, step: steps.length });
+ continue;
+ }
+
if (repeats > REPEAT_LIMIT) {
logger.warn(`ReAct loop repeated "${key}" ${repeats} times; stopping.`);
// Careful not to overclaim failure: the loop can repeat itself *after* doing
@@ -573,6 +642,7 @@ async function run(options) {
}
: await execute(action);
steps.push({ action, result });
+ workingSet.record(action, result, steps.length);
emit({ type: 'observation', step: steps.length, action, result });
// A refused write changed nothing, so the corrected retry the hint just asked
diff --git a/app/agent/workingSet.js b/app/agent/workingSet.js
new file mode 100644
index 0000000..b28d162
--- /dev/null
+++ b/app/agent/workingSet.js
@@ -0,0 +1,301 @@
+'use strict';
+
+/**
+ * What the agent already has, stated as fact rather than asked for as judgement.
+ *
+ * ## The two failures this exists for
+ *
+ * Both evaluation sessions of 0.7.0 lost most of their time or all of their output to
+ * the same missing thing, at opposite ends of the model range. See
+ * `doc/SESSION-ANALYSIS-0.7.0.md` for the counted version.
+ *
+ * **`qwen3.5:0.8b` never wrote a single file.** Five of its seven sessions ended
+ * `repeating`, four of them at exactly two steps: `list_files`, `list_files`,
+ * `list_files` — and the repeat guard ended the run. Twelve of its twenty-two total
+ * steps were `list_files`.
+ *
+ * **`qwen3.5:4b` finished the task and spent 88 minutes doing it**, 97% of that inside
+ * the model. 73 of its 126 steps were `read_file`, against 21 writes; the audit log has
+ * 263 read entries across 25 distinct paths. `App.jsx` was read 28 times and written 4.
+ * A binary PNG was read into the prompt 13 times.
+ *
+ * ## Why the existing hints did not prevent either
+ *
+ * They fired, correctly, every time. `nextStepHint` already says *"You now know what is
+ * in the project"* after a listing and *"Do NOT do it again"* on a repeat, and
+ * `reactLoop`'s header documents both. The models did it again anyway.
+ *
+ * That is the finding, and it is not really about model quality: **every anti-repetition
+ * device in the loop is a sentence addressed to the model's judgement, evaluated against
+ * a context that no longer contains the thing it is describing.** A hint says "you
+ * already have the listing" while the listing itself has scrolled out of the window. The
+ * model is being asked to take the loop's word for it, and reaching for the tool is the
+ * cheaper way to be sure.
+ *
+ * So this module does not add a firmer sentence. It keeps the **record**, and renders
+ * it back as a standing block: the paths, the commands, the outcomes. A model that can
+ * see `src/App.jsx` in a list titled "you have already read these" does not have to
+ * decide whether to believe a claim about its own past — the past is in front of it.
+ *
+ * ## Why one module for both tiers
+ *
+ * `stepBrief` does something close to this and does it well, but only inside
+ * `_runWithTodos`, which requires `canPlanTodos`, which requires ≥ 2B parameters. The
+ * model that needs the reminder most is excluded from it by a threshold. This runs off
+ * the step trace instead, so it costs nothing to give a 0.8B model the same footing as a
+ * 4B one.
+ *
+ * @module agent/workingSet
+ */
+
+const { neutralize } = require('../core/memoryStore');
+
+/** Paths listed in a section before the rest are elided. */
+const MAX_PATHS_SHOWN = 10;
+
+/** Commands recalled, most recent last. */
+const MAX_COMMANDS_SHOWN = 5;
+
+/** Longest single path rendered; a deep path adds nothing past this. */
+const MAX_PATH_CHARS = 80;
+
+/**
+ * Actions that fetch something the agent could already be holding.
+ *
+ * These are the ones worth tracking for redundancy. A write is tracked too, but for the
+ * opposite reason — it is evidence that a file *exists*, which is what the next step
+ * needs in order to import from it.
+ */
+const FETCHING_ACTIONS = new Set(['read_file', 'list_files', 'search_workspace']);
+
+/**
+ * Reconnaissance: read-only, cheap, and idempotent.
+ *
+ * Kept separate from `FETCHING_ACTIONS` — which it currently equals — because the two
+ * are asked different questions and will not stay equal. This set answers "is repeating
+ * this actually harmful?", and the answer governs whether a repeat is worth ending a
+ * user's session over. A repeated `run_script` is a different matter: it can install
+ * packages, start servers, and cost real time.
+ */
+const RECON_ACTIONS = new Set(['read_file', 'list_files', 'search_workspace']);
+
+/**
+ * @param {string} action
+ * @returns {boolean}
+ */
+function isRecon(action) {
+ return RECON_ACTIONS.has(String(action));
+}
+
+/**
+ * @param {string} path
+ * @returns {string}
+ */
+function shortPath(path) {
+ const text = String(path || '').trim();
+ if (text.length <= MAX_PATH_CHARS) return text;
+ return `…${text.slice(-(MAX_PATH_CHARS - 1))}`;
+}
+
+/**
+ * @param {string[]} paths
+ * @returns {string}
+ */
+function renderPaths(paths) {
+ const shown = paths.slice(-MAX_PATHS_SHOWN).map(shortPath);
+ const elided = paths.length - shown.length;
+ return elided > 0 ? `${shown.join(', ')} (+${elided} more)` : shown.join(', ');
+}
+
+class WorkingSet {
+ constructor() {
+ /** @type {Map} Read, by path. */
+ this.read = new Map();
+ /** @type {Map} Written, by path. */
+ this.written = new Map();
+ /** @type {Map} Directories listed, path → times. */
+ this.listed = new Map();
+ /** @type {Array<{command: string, cwd: string, ok: boolean}>} */
+ this.commands = [];
+ /** @type {Array<{action: string, path: string, why: string}>} What went wrong. */
+ this.struggles = [];
+ /** Paths deleted, so the set never claims a gone file still exists. */
+ this.deleted = new Set();
+ }
+
+ /**
+ * Fold one executed step into the record.
+ *
+ * Called with what the loop already has — the action it sent and the result it got —
+ * so nothing here depends on the model describing its own behaviour accurately.
+ *
+ * @param {import('../core/outputParser').ParsedAction} action
+ * @param {import('./toolRegistry').ToolResult} result
+ * @param {number} step 1-based index of this step.
+ */
+ record(action, result, step) {
+ const name = String(action && action.action);
+ const path = action && action.path ? String(action.path) : '';
+ const ok = Boolean(result && result.ok);
+
+ if (!ok) {
+ // Only the failures worth restating: a model that is told what it struggled with
+ // stops re-attempting it, and the user's spec for the small-model reminder asks
+ // for this by name.
+ const why = String((result && result.observation) || '')
+ .split('\n')[0]
+ .trim();
+ this.struggles.push({ action: name, path, why });
+ return;
+ }
+
+ switch (name) {
+ case 'read_file':
+ if (path) this.read.set(path, { step, bytes: Number(result.bytes) || 0 });
+ break;
+ case 'list_files':
+ // The root is listed as "." or "", and both mean the same folder. Normalising
+ // matters because the whole point is recognising the repeat.
+ this.listed.set(path || '.', (this.listed.get(path || '.') || 0) + 1);
+ break;
+ case 'search_workspace':
+ break;
+ case 'write_file':
+ if (path) {
+ this.written.set(path, { step, created: !this.read.has(path) && !this.written.has(path) });
+ this.deleted.delete(path);
+ // A file just written is a file whose contents the model now knows: it sent
+ // them. Recording the read too is what stops the "write then immediately read
+ // back the same file" pair that cost the 4B session a third of its steps.
+ this.read.set(path, { step, bytes: (action.code || '').length });
+ }
+ break;
+ case 'delete_file':
+ if (path) {
+ this.deleted.add(path);
+ this.read.delete(path);
+ this.written.delete(path);
+ }
+ break;
+ case 'run_script':
+ if (action.command) {
+ this.commands.push({ command: String(action.command), cwd: String(action.cwd || ''), ok });
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Has this exact path already been fetched, and is it still there?
+ *
+ * @param {string} path
+ * @returns {boolean}
+ */
+ hasRead(path) {
+ const key = String(path || '');
+ return this.read.has(key) && !this.deleted.has(key);
+ }
+
+ /**
+ * How many times this folder has been listed.
+ *
+ * @param {string} path
+ * @returns {number}
+ */
+ timesListed(path) {
+ return this.listed.get(String(path || '') || '.') || 0;
+ }
+
+ /**
+ * Nothing recorded yet — the block would be all headings and no content.
+ *
+ * Deletions and struggles count. A session whose only step was a failed read has
+ * nothing in hand but does have something worth saying, and an earlier version of this
+ * getter checked only the four "has" collections — so the one turn where the model
+ * most needed telling what had just gone wrong was the turn that rendered nothing.
+ */
+ get isEmpty() {
+ return (
+ this.read.size === 0 &&
+ this.written.size === 0 &&
+ this.listed.size === 0 &&
+ this.commands.length === 0 &&
+ this.deleted.size === 0 &&
+ this.struggles.length === 0
+ );
+ }
+
+ /**
+ * The standing block: what is already in hand, and what is therefore pointless.
+ *
+ * Phrased throughout as statements of fact about the session rather than as
+ * instructions to the model, with one imperative at the end. A 0.8B model handed six
+ * imperatives obeys the last one; handed a list of paths under a heading, it has
+ * something it can check an intention against.
+ *
+ * @param {object} [opts]
+ * @param {boolean} [opts.includeStruggles] Off for the smallest budgets, where the
+ * failures crowd out the paths and the paths are what prevent the loop.
+ * @returns {string}
+ */
+ render(opts = {}) {
+ if (this.isEmpty) return '';
+
+ /** @type {string[]} */
+ const lines = [];
+
+ const readPaths = [...this.read.keys()].filter((path) => !this.deleted.has(path));
+ if (readPaths.length > 0) {
+ lines.push(`- Files you have ALREADY READ (you have their contents): ${renderPaths(readPaths)}`);
+ }
+
+ const writtenPaths = [...this.written.keys()];
+ if (writtenPaths.length > 0) {
+ lines.push(`- Files you have ALREADY WRITTEN (they exist — edit, do not recreate): ${renderPaths(writtenPaths)}`);
+ }
+
+ const listedPaths = [...this.listed.keys()];
+ if (listedPaths.length > 0) {
+ lines.push(`- Folders you have ALREADY LISTED (you know what is in them): ${renderPaths(listedPaths)}`);
+ }
+
+ if (this.deleted.size > 0) {
+ lines.push(`- Files you have DELETED (they are gone): ${renderPaths([...this.deleted])}`);
+ }
+
+ if (this.commands.length > 0) {
+ const recent = this.commands.slice(-MAX_COMMANDS_SHOWN).map((entry) => {
+ const where = entry.cwd ? ` in ${shortPath(entry.cwd)}` : '';
+ return `\`${entry.command}\`${where}${entry.ok ? '' : ' (failed)'}`;
+ });
+ lines.push(`- Commands you have ALREADY RUN: ${recent.join('; ')}`);
+ }
+
+ if (opts.includeStruggles && this.struggles.length > 0) {
+ const last = this.struggles[this.struggles.length - 1];
+ const target = last.path ? ` on ${shortPath(last.path)}` : '';
+ lines.push(`- What went wrong last time: ${last.action}${target} — ${neutralize(last.why, { maxChars: 160 })}`);
+ }
+
+ // A session whose only record is a struggle, rendered with `includeStruggles` off,
+ // reaches here with every section empty. Emitting the heading and the closing
+ // imperative around nothing would spend tokens telling the model not to re-fetch an
+ // empty list.
+ if (lines.length === 0) return '';
+
+ return `WHAT YOU ALREADY HAVE:\n${lines.join('\n')}\nDo not fetch any of it again. Use it.`;
+ }
+}
+
+module.exports = {
+ WorkingSet,
+ isRecon,
+ renderPaths,
+ shortPath,
+ FETCHING_ACTIONS,
+ RECON_ACTIONS,
+ MAX_PATHS_SHOWN,
+ MAX_COMMANDS_SHOWN,
+};
diff --git a/test/unit/agentSession.test.js b/test/unit/agentSession.test.js
index 0468c24..d87f28d 100644
--- a/test/unit/agentSession.test.js
+++ b/test/unit/agentSession.test.js
@@ -593,7 +593,12 @@ describe('AgentSession', () => {
const client = scriptedClient([
json({ thought: 'look', action: 'read_file', path: 'src/app.js' }),
json({ thought: 'look again', action: 'read_file', path: 'src/app.js' }),
+ // The third repeat is answered rather than fatal now — see `agent/workingSet`,
+ // which hands the content back once instead of ending a run over a read-only
+ // call. The fourth is what reaches the guard, so the script needs one more read
+ // than it did to arrive at the same stop this test is about.
json({ thought: 'and again', action: 'read_file', path: 'src/app.js' }),
+ json({ thought: 'once more', action: 'read_file', path: 'src/app.js' }),
// The follow-up planning call, once the loop has given up.
'1. Add validation to src/app.js\n2. Remove src/old.js',
]);
diff --git a/test/unit/workingSet.test.js b/test/unit/workingSet.test.js
new file mode 100644
index 0000000..9f15e53
--- /dev/null
+++ b/test/unit/workingSet.test.js
@@ -0,0 +1,271 @@
+'use strict';
+
+/**
+ * What the agent already has, and what the loop does when it asks for it again.
+ *
+ * The reproduction at the bottom is the `qwen3.5:0.8b` session shape verbatim: three
+ * identical `list_files` calls, which on 0.7.0 ended the run at two steps with nothing
+ * written. It happened seven times in one evaluation.
+ */
+
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const { WorkingSet, isRecon, shortPath } = require('../../app/agent/workingSet');
+const { AgentSession } = require('../../app/agent/agentSession');
+const { PermissionGate } = require('../../app/security/permissionGate');
+const { PermissionModes } = require('../../app/security/permissionModes');
+const { AuditLog } = require('../../app/security/auditLog');
+
+const TIER_B = { tier: 'B', strategy: 'react', label: 'Lite', model: 'qwen3.5:0.8b' };
+
+/** @param {Array} replies */
+function scriptedClient(replies) {
+ return {
+ calls: 0,
+ prompts: /** @type {string[]} */ ([]),
+ async chat(body) {
+ this.prompts.push(JSON.stringify(body.messages));
+ const reply = replies[Math.min(this.calls, replies.length - 1)];
+ this.calls += 1;
+ return { message: typeof reply === 'string' ? { content: reply } : reply };
+ },
+ };
+}
+
+/** @param {object} action */
+const json = (action) => JSON.stringify(action);
+
+const ok = (observation) => ({ ok: true, observation });
+
+describe('workingSet', () => {
+ describe('recording', () => {
+ it('remembers a file it read', () => {
+ const set = new WorkingSet();
+ set.record({ action: 'read_file', path: 'src/App.jsx' }, ok('contents'), 1);
+
+ assert.strictEqual(set.hasRead('src/App.jsx'), true);
+ assert.match(set.render(), /ALREADY READ.*src\/App\.jsx/s);
+ });
+
+ it('counts a file it wrote as a file it has', () => {
+ // The pair that cost the 4B session most: write App.jsx, then immediately read it
+ // back. The model sent those contents — it does not need them returned.
+ const set = new WorkingSet();
+ set.record({ action: 'write_file', path: 'src/App.jsx', code: 'x' }, ok('written'), 1);
+
+ assert.strictEqual(set.hasRead('src/App.jsx'), true);
+ assert.match(set.render(), /ALREADY WRITTEN.*src\/App\.jsx/s);
+ });
+
+ it('treats the root listed as "." and "" as one folder', () => {
+ const set = new WorkingSet();
+ set.record({ action: 'list_files', path: '' }, ok('a\nb'), 1);
+ set.record({ action: 'list_files', path: '.' }, ok('a\nb'), 2);
+
+ assert.strictEqual(set.timesListed('.'), 2, 'the root was tracked as two different folders');
+ });
+
+ it('stops claiming a deleted file still exists', () => {
+ const set = new WorkingSet();
+ set.record({ action: 'read_file', path: 'src/old.js' }, ok('contents'), 1);
+ set.record({ action: 'delete_file', path: 'src/old.js' }, ok('deleted'), 2);
+
+ assert.strictEqual(set.hasRead('src/old.js'), false);
+ assert.match(set.render(), /DELETED.*src\/old\.js/s);
+ assert.doesNotMatch(set.render(), /ALREADY READ/);
+ });
+
+ it('records a failed step as a struggle rather than as something it has', () => {
+ const set = new WorkingSet();
+ set.record({ action: 'read_file', path: 'src/nope.js' }, { ok: false, observation: 'ENOENT: no such file' }, 1);
+
+ assert.strictEqual(set.hasRead('src/nope.js'), false);
+ assert.match(set.render({ includeStruggles: true }), /went wrong.*ENOENT/s);
+ });
+
+ it('keeps the folder a command ran in, because that is what made it work', () => {
+ const set = new WorkingSet();
+ set.record({ action: 'run_script', command: 'npm install', cwd: 'todo-glass-app' }, ok('added 24'), 1);
+
+ assert.match(set.render(), /npm install.*in todo-glass-app/s);
+ });
+
+ it('renders nothing at all before anything has happened', () => {
+ assert.strictEqual(new WorkingSet().render(), '');
+ });
+
+ it('elides a long list rather than pasting a whole workspace into every turn', () => {
+ const set = new WorkingSet();
+ for (let i = 0; i < 30; i += 1) {
+ set.record({ action: 'read_file', path: `src/file${i}.js` }, ok('x'), i + 1);
+ }
+
+ const rendered = set.render();
+ assert.match(rendered, /\+20 more/);
+ assert.ok(rendered.length < 900, `the block grew without bound (${rendered.length} chars)`);
+ });
+ });
+
+ describe('isRecon', () => {
+ it('counts read-only lookups and nothing else', () => {
+ for (const action of ['read_file', 'list_files', 'search_workspace']) {
+ assert.strictEqual(isRecon(action), true, `${action} should be recon`);
+ }
+ // Repeating one of these can install packages or start a server. "It was only a
+ // repeat" is no comfort there.
+ for (const action of ['write_file', 'run_script', 'delete_file', 'run_tests']) {
+ assert.strictEqual(isRecon(action), false, `${action} must not be treated as recon`);
+ }
+ });
+ });
+
+ describe('shortPath', () => {
+ it('trims from the front, because the filename is the informative end', () => {
+ const long = `src/${'deeply/'.repeat(20)}App.jsx`;
+ assert.ok(shortPath(long).length <= 81);
+ assert.match(shortPath(long), /App\.jsx$/);
+ });
+ });
+
+ describe('in the loop', () => {
+ /** @type {string} */
+ let root;
+
+ beforeEach(() => {
+ root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hiraya-ws-')));
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
+ fs.writeFileSync(path.join(root, 'src', 'app.js'), 'export function app() {\n return 1;\n}\n');
+ });
+
+ afterEach(() => fs.rmSync(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }));
+
+ function makeSession(client) {
+ const modes = new PermissionModes({ initial: { autoEdit: true } });
+ return new AgentSession({
+ client: /** @type {any} */ (client),
+ model: 'qwen3.5:0.8b',
+ capability: TIER_B,
+ gate: new PermissionGate({
+ workspaceRoot: root,
+ modes,
+ auditLog: new AuditLog(root),
+ confirm: async () => true,
+ }),
+ workspaceRoot: root,
+ thinkingCapacity: 'medium',
+ sessionId: '1',
+ });
+ }
+
+ it('answers a repeated listing instead of ending the session', async () => {
+ // The 0.7.0 `qwen3.5:0.8b` session, verbatim: list, list, list. It ended four
+ // separate runs at exactly two steps with nothing written, seven times across the
+ // evaluation. The third call must now be answered, not fatal.
+ const client = scriptedClient([
+ json({ action: 'list_files', path: '.' }),
+ json({ action: 'list_files', path: '.' }),
+ json({ action: 'list_files', path: '.' }),
+ json({ action: 'write_file', path: 'src/app.js', code: 'export function app() {\n return 2;\n}\n' }),
+ json({ action: 'done', summary: 'Changed the return value.' }),
+ ]);
+
+ const result = await makeSession(client).run('Make app() return 2', { mode: 'agent' });
+
+ assert.notStrictEqual(result.stopReason, 'repeating', 'a repeated listing still ended the run');
+ assert.strictEqual(result.stopReason, 'done');
+ assert.match(fs.readFileSync(path.join(root, 'src', 'app.js'), 'utf8'), /return 2/);
+ });
+
+ it('tells the model to write, and hands back what it already had', async () => {
+ const client = scriptedClient([
+ json({ action: 'read_file', path: 'src/app.js' }),
+ json({ action: 'read_file', path: 'src/app.js' }),
+ json({ action: 'read_file', path: 'src/app.js' }),
+ json({ action: 'write_file', path: 'src/app.js', code: 'export function app() {\n return 2;\n}\n' }),
+ json({ action: 'done', summary: 'Done.' }),
+ ]);
+
+ await makeSession(client).run('Make app() return 2', { mode: 'agent' });
+
+ const afterSubstitution = client.prompts[3];
+ assert.match(afterSubstitution, /STOP/, 'the substitution said nothing forceful');
+ assert.match(afterSubstitution, /write_file/, 'it did not name the next move');
+ // The content, not just the assertion about it — the reason a hint alone failed
+ // is that it described something the model could no longer see.
+ assert.match(afterSubstitution, /return 1/, 'the file it already had was not handed back');
+ });
+
+ it('still stops a model that ignores the substitution', async () => {
+ // Forgiveness is capped at one. A model that repeats with the content and an
+ // instruction both in front of it is stuck, not disoriented.
+ const client = scriptedClient([json({ action: 'list_files', path: '.' })]);
+
+ const result = await makeSession(client).run('Do something', { mode: 'agent' });
+
+ assert.strictEqual(result.stopReason, 'repeating');
+ });
+
+ it('does not forgive a repeated command, which can cost real time', async () => {
+ const client = scriptedClient([json({ action: 'run_script', command: 'npm install' })]);
+
+ const result = await makeSession(client).run('Install the dependencies', { mode: 'agent' });
+
+ assert.strictEqual(result.stopReason, 'repeating', 'a repeated run_script was forgiven');
+ });
+
+ it('carries exactly one working-set block on Tier A, however long the run', async () => {
+ // Tier A keeps the whole exchange in `messages`, so a block pushed each turn would
+ // leave a trail of stale copies — each accurate when written and contradicted by
+ // the next one down. There must be one, and it must be the current one.
+ const calls = [];
+ const client = {
+ prompts: [],
+ async chat(body) {
+ this.prompts.push(body.messages);
+ const reply = calls.shift();
+ return { message: reply || { content: 'All done.' } };
+ },
+ };
+ for (const file of ['src/app.js', 'src/app.js', 'src/app.js']) {
+ calls.push({ tool_calls: [{ function: { name: 'read_file', arguments: { path: file } } }] });
+ }
+
+ const session = new AgentSession({
+ client: /** @type {any} */ (client),
+ model: 'qwen3.5:4b',
+ capability: { tier: 'A', strategy: 'native', label: 'Agentic', model: 'qwen3.5:4b' },
+ gate: new PermissionGate({
+ workspaceRoot: root,
+ modes: new PermissionModes({ initial: { autoEdit: true } }),
+ auditLog: new AuditLog(root),
+ confirm: async () => true,
+ }),
+ workspaceRoot: root,
+ thinkingCapacity: 'medium',
+ sessionId: '1',
+ });
+ await session.run('Look at the file', { mode: 'agent' });
+
+ const last = client.prompts[client.prompts.length - 1];
+ const blocks = last.filter((m) => typeof m.content === 'string' && m.content.includes('WHAT YOU ALREADY HAVE'));
+ assert.strictEqual(blocks.length, 1, `the block accumulated (${blocks.length} copies)`);
+ assert.strictEqual(last[last.length - 1], blocks[0], 'the block drifted away from the decision');
+ assert.match(blocks[0].content, /src\/app\.js/);
+ });
+
+ it('puts what the agent holds into the next prompt', async () => {
+ const client = scriptedClient([
+ json({ action: 'read_file', path: 'src/app.js' }),
+ json({ action: 'done', summary: 'Read it.' }),
+ ]);
+
+ await makeSession(client).run('Look at the file', { mode: 'agent' });
+
+ assert.match(client.prompts[1], /ALREADY READ/);
+ assert.match(client.prompts[1], /src\/app\.js/);
+ });
+ });
+});
From 6a512de0fe8ea59dee22c945dd4b13b047ada3e6 Mon Sep 17 00:00:00 2001
From: jaymar921
Date: Fri, 14 Aug 2026 20:27:10 +0800
Subject: [PATCH 3/5] feat(0.8.0): make the step trace a live panel that says
why
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The trace showed `read_file src/App.jsx ok` — the tool identifier the model is
required to emit, the path, and an outcome. Two things were wrong with that.
It was collapsed always. The 0.7.0 sessions measured 42 seconds per step and 88
minutes for one task, so the panel sat silent for minutes at a time and the user's
first sight of a run going wrong was the summary at the end. The steps are the
only evidence available while interrupting is still worth doing. It now opens on
the first step and collapses when the turn ends — unless the user has clicked it
themselves, after which we stop deciding for them.
And it never showed why. Each row now carries the model's own stated reason for
the step, which both loops already captured as `thought` and nothing rendered.
Without it, eight reads of one file look exactly like eight reads of eight.
`read_file` is also gone from the panel in favour of "Reading". The identifier is
part of the tool protocol and does not belong in the surface whose job is to
explain the run.
The row logic is a pure `describeStep` so it is testable — the webview's DOM
assembly is not reachable from the unit suite, and adding jsdom to a
privacy-first extension to render one row is a poor trade.
Two details worth naming. The `toggle` event is the obvious hook for "did the user
open this" and the wrong one: it also fires on a programmatic `open`, and fires
asynchronously, so a flag set around the assignment is not reliably cleared before
it arrives. A click on the summary is unambiguous. And the verb lookup is a Map,
because its key is model output — a plain object lookup reaches the prototype, so
an action named `constructor` returns `Function`, survives the `||` fallback, and
renders a function's source as the name of a step.
---
app/webview/components/messageBubble.js | 121 ++++++++++++++++++++++--
app/webview/main.js | 4 +
app/webview/style.css | 19 ++++
test/unit/webviewComponents.test.js | 60 ++++++++++++
4 files changed, 197 insertions(+), 7 deletions(-)
diff --git a/app/webview/components/messageBubble.js b/app/webview/components/messageBubble.js
index 0c0c865..d518b1b 100644
--- a/app/webview/components/messageBubble.js
+++ b/app/webview/components/messageBubble.js
@@ -60,11 +60,81 @@ export function appendImages(body, images) {
}
/**
- * The collapsible step trace inside an assistant message.
+ * What each tool call is called, in the language of the thing it does.
*
- * Collapsed by default: a fifteen-step session would otherwise bury the answer the
- * user actually asked for. The summary line carries enough to decide whether to open
- * it.
+ * The trace used to print the tool name verbatim — `read_file`, `run_script`. That is
+ * the identifier the model is required to emit, and showing it to the user leaks an
+ * implementation detail into the one surface that is supposed to explain the run. A
+ * step reading "Reading src/App.jsx" needs no glossary.
+ *
+ * A Map rather than an object literal, because the key is model output. A plain
+ * `ACTION_VERBS[name]` lookup reaches the prototype, so a model emitting the action
+ * `"constructor"` gets `Function` back — truthy, so it survives the `||` fallback — and
+ * the panel renders a function's source as the name of a step. A Map has no prototype
+ * keys to find.
+ */
+const ACTION_VERBS = new Map([
+ ['read_file', 'Reading'],
+ ['write_file', 'Editing'],
+ ['list_files', 'Listing'],
+ ['search_workspace', 'Searching'],
+ ['run_script', 'Running'],
+ ['run_tests', 'Testing'],
+ ['delete_file', 'Deleting'],
+ ['create_folder', 'Creating folder'],
+ ['delete_folder', 'Removing folder'],
+]);
+
+/** Longest status message shown before it is cut; the title carries the rest. */
+const MAX_STATUS_CHARS = 110;
+
+/**
+ * One step as the three things the panel shows: what, to what, and why.
+ *
+ * Split out from the rendering because this is the part with decisions in it, and the
+ * webview's DOM assembly is not reachable from the unit suite — see the header of
+ * `test/unit/webviewComponents.test.js`. Building the nodes from this is trivial and
+ * uninteresting; choosing the words is not.
+ *
+ * @param {{action: string, path?: string, command?: string, query?: string, thought?: string}} action
+ * @returns {{verb: string, target: string, status: string, full: string}}
+ */
+export function describeStep(action) {
+ const name = String((action && action.action) || '');
+ // Collapsed rather than trimmed: a `thought` arrives as free text from the model and
+ // routinely contains newlines, which would break a single-line row into several.
+ const full = String((action && action.thought) || '').replace(/\s+/g, ' ').trim();
+
+ return {
+ verb: ACTION_VERBS.get(name) || name,
+ target: String((action && (action.path || action.command || action.query)) || ''),
+ status: full.length > MAX_STATUS_CHARS ? `${full.slice(0, MAX_STATUS_CHARS - 1)}…` : full,
+ full,
+ };
+}
+
+/**
+ * The live step panel inside an assistant message.
+ *
+ * ## Why it opens while the run is happening
+ *
+ * It used to be collapsed always, on the reasoning that a fifteen-step session would
+ * bury the answer. That is right once there *is* an answer and wrong until then. The
+ * 0.7.0 sessions measured 42 seconds per step on a 4B model and 88 minutes for one
+ * task — so for minutes at a time the panel showed a single thinking indicator, and
+ * the user's first sight of a run going wrong was the summary at the end. The steps
+ * are the only evidence available while it is still worth interrupting.
+ *
+ * So it opens when the first step arrives and collapses on `finish`, unless the user
+ * has touched it — `_userToggled` exists to make sure a panel someone deliberately
+ * opened is never shut on them.
+ *
+ * ## Why each row carries a status message
+ *
+ * A row is three things: what is being done, what it is being done to, and why. The
+ * "why" is the model's own stated reason for the step, which the loops already capture
+ * as `thought` and which nothing was showing. Without it a trace of eight reads of the
+ * same file looks identical to eight reads of different ones.
*/
export class TraceView {
constructor() {
@@ -81,13 +151,24 @@ export class TraceView {
this.count = 0;
/** @type {Map} */
this.rows = new Map();
+ /** Set once the user opens or closes it themselves; we stop deciding after that. */
+ this._userToggled = false;
+ // The `toggle` event is the obvious hook and the wrong one: it also fires when
+ // `open` is assigned in code, and it fires asynchronously, so a "this was us" flag
+ // set around the assignment is not reliably cleared before it arrives. A click on
+ // the summary is unambiguous — nothing but a person produces one.
+ this.summary.addEventListener('click', () => {
+ this._userToggled = true;
+ });
}
/**
* @param {number} step
- * @param {{action: string, path?: string, command?: string, query?: string}} action
+ * @param {{action: string, path?: string, command?: string, query?: string, thought?: string}} action
*/
addAction(step, action) {
+ const described = describeStep(action);
+
const row = document.createElement('div');
row.className = 'step is-active';
@@ -97,18 +178,44 @@ export class TraceView {
const name = document.createElement('span');
name.className = 'step-action';
- name.textContent = action.action;
+ name.textContent = described.verb;
const target = document.createElement('span');
target.className = 'step-target';
- target.textContent = action.path || action.command || action.query || '';
+ target.textContent = described.target;
row.appendChild(n);
row.appendChild(name);
row.appendChild(target);
+
+ // The model's stated reason for the step. Model output — data, never markup.
+ if (described.status) {
+ const status = document.createElement('span');
+ status.className = 'step-status';
+ status.textContent = described.status;
+ status.title = described.full;
+ row.appendChild(status);
+ }
+
this.list.appendChild(row);
this.rows.set(step, row);
this.count = Math.max(this.count, step);
+ // Opened on the first step rather than at construction: a conversational turn
+ // builds a TraceView and never adds to it, and an empty open panel reads as a
+ // promise of work that is not coming.
+ if (!this._userToggled) this.el.open = true;
+ this._retitle();
+ }
+
+ /**
+ * The run is over: stop showing the panel expanded, and stop marking a step active.
+ *
+ * A step left `is-active` after a cancelled or failed run keeps the accent on a step
+ * that is not running, which is the one thing the accent is for.
+ */
+ finish() {
+ for (const row of this.rows.values()) row.classList.remove('is-active');
+ if (!this._userToggled) this.el.open = false;
this._retitle();
}
diff --git a/app/webview/main.js b/app/webview/main.js
index 5a08bcf..0881b7a 100644
--- a/app/webview/main.js
+++ b/app/webview/main.js
@@ -187,6 +187,10 @@ function finishAssistantMessage() {
state.indicator.dispose();
state.indicator = null;
}
+ // Every way a turn can end comes through here — done, error, and cancellation — so
+ // this is the one place that reliably closes the live step panel and clears the
+ // accent off whichever step was running when it stopped.
+ if (state.trace) state.trace.finish();
}
const handlers = {
diff --git a/app/webview/style.css b/app/webview/style.css
index eec0b02..32de296 100644
--- a/app/webview/style.css
+++ b/app/webview/style.css
@@ -348,10 +348,29 @@ select.control {
overflow-wrap: anywhere;
}
+/* The model's stated reason for the step. Muted and italic so a row reads as
+ "Reading src/App.jsx" first and explains itself second, and truncated with an
+ ellipsis rather than wrapping — a run of eight steps should stay eight lines. */
+.step-status {
+ color: var(--muted);
+ font-style: italic;
+ font-size: var(--fs-xs);
+ min-width: 0;
+ flex: 1 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.step-status::before {
+ content: '— ';
+}
+
.step-result {
margin-left: auto;
flex: none;
font-size: var(--fs-xs);
+ padding-left: var(--sp-2);
}
.step-result.ok {
diff --git a/test/unit/webviewComponents.test.js b/test/unit/webviewComponents.test.js
index 453b987..3d8f0d0 100644
--- a/test/unit/webviewComponents.test.js
+++ b/test/unit/webviewComponents.test.js
@@ -269,6 +269,66 @@ describe('webview markdown rendering', () => {
});
});
+describe('step panel rows', () => {
+ /** @type {(action: object) => {verb: string, target: string, status: string, full: string}} */
+ let describeStep;
+
+ before(async () => {
+ // eslint-disable-next-line no-unsanitized/method
+ ({ describeStep } = await import(moduleUrl('components/messageBubble.js')));
+ });
+
+ it('names the action in the language of what it does', () => {
+ // `read_file` is the identifier the model is required to emit. Showing it to the
+ // user leaks the tool protocol into the surface that is meant to explain the run.
+ assert.strictEqual(describeStep({ action: 'read_file', path: 'README.md' }).verb, 'Reading');
+ assert.strictEqual(describeStep({ action: 'run_script', command: 'npm install' }).verb, 'Running');
+ assert.strictEqual(describeStep({ action: 'write_file', path: 'src/App.jsx' }).verb, 'Editing');
+ });
+
+ it('falls back to the raw name for an action it does not know', () => {
+ assert.strictEqual(describeStep({ action: 'some_new_tool' }).verb, 'some_new_tool');
+ });
+
+ it('shows what the step is being done to, whichever field carries it', () => {
+ assert.strictEqual(describeStep({ action: 'read_file', path: 'src/App.jsx' }).target, 'src/App.jsx');
+ assert.strictEqual(describeStep({ action: 'run_script', command: 'npm run build' }).target, 'npm run build');
+ assert.strictEqual(describeStep({ action: 'search_workspace', query: 'useTodos' }).target, 'useTodos');
+ assert.strictEqual(describeStep({ action: 'list_files' }).target, '');
+ });
+
+ it('carries the reason the model gave for the step', () => {
+ const row = describeStep({ action: 'read_file', path: 'README.md', thought: 'extracting project structure' });
+ assert.strictEqual(row.status, 'extracting project structure');
+ });
+
+ it('collapses a multi-line reason so one step stays one row', () => {
+ const row = describeStep({ action: 'read_file', path: 'a.js', thought: 'first line\n\nsecond line' });
+ assert.strictEqual(row.status, 'first line second line');
+ });
+
+ it('cuts a long reason but keeps the whole of it for the tooltip', () => {
+ const long = `I need to check ${'a lot of things '.repeat(20)}`;
+ const row = describeStep({ action: 'read_file', path: 'a.js', thought: long });
+
+ assert.ok(row.status.length <= 110, `the row would not fit on one line (${row.status.length})`);
+ assert.match(row.status, /…$/);
+ assert.ok(row.full.length > row.status.length, 'the full reason was lost');
+ });
+
+ it('reports no status at all when the model gave no reason', () => {
+ assert.strictEqual(describeStep({ action: 'read_file', path: 'a.js' }).status, '');
+ assert.strictEqual(describeStep({ action: 'read_file', path: 'a.js', thought: ' ' }).status, '');
+ });
+
+ it('survives an action object with nothing in it', () => {
+ const row = describeStep({});
+ assert.strictEqual(row.verb, '');
+ assert.strictEqual(row.target, '');
+ assert.strictEqual(row.status, '');
+ });
+});
+
describe('thinking indicator lines', () => {
/** @type {any} */
let mod;
From c90007ea51a1ee49511f178aad9b824d076b1cc3 Mon Sep 17 00:00:00 2001
From: jaymar921
Date: Fri, 14 Aug 2026 20:34:28 +0800
Subject: [PATCH 4/5] docs(0.8.0): version bump, two new images, and an honest
pre-release note
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The hero offered "Install from the VS Code Marketplace". The extension is not on
the Marketplace, so the button in the picture was the one thing in it that was not
true. It now reads "Download from GitHub Releases" and carries the pre-release
note, and the README opens with the same, pointing at the step that already
documented the real install.
Two new images, both built from the same 1280x720 HTML sources as the existing
pair:
- live-session.png — the step panel mid-run, six steps of a TODO build with the
action, the file, and the model's stated reason on each row.
- knows-what-it-has.png — the 0.8B before-and-after, using the measured numbers:
three identical list_files and the run ends, 5 of 7 sessions, 0 files written,
against a repeat that is answered and a third step that writes.
The "New in" tag moves from "Big requests become a checklist" to "Agentic on every
model", which is the card 0.8.0 actually changed.
The regeneration instructions gained the two Windows failure modes that cost time
here, both of which fail silently — Chromium reports success and writes nothing.
The page has to be a percent-encoded file:/// URL, and --screenshot will not write
to a path containing a space, which every path in this repo does.
---
CHANGELOG.md | 94 +++++++++++
README.md | 38 ++++-
docs/images$name.png | Bin 0 -> 71667 bytes
docs/images/capabilities.png | Bin 478533 -> 479841 bytes
docs/images/hero-offline-agent.png | Bin 379057 -> 391762 bytes
docs/images/knows-what-it-has.png | Bin 0 -> 310369 bytes
docs/images/live-session.png | Bin 0 -> 333176 bytes
docs/images/src/README.md | 19 ++-
docs/images/src/capabilities.html | 6 +-
docs/images/src/hero-offline-agent.html | 6 +-
docs/images/src/knows-what-it-has.html | 166 +++++++++++++++++++
docs/images/src/live-session.html | 207 ++++++++++++++++++++++++
package.json | 2 +-
13 files changed, 526 insertions(+), 12 deletions(-)
create mode 100644 docs/images$name.png
create mode 100644 docs/images/knows-what-it-has.png
create mode 100644 docs/images/live-session.png
create mode 100644 docs/images/src/knows-what-it-has.html
create mode 100644 docs/images/src/live-session.html
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17cfecc..94c75a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,100 @@ All notable changes to HirayaCoder are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.8.0] — unreleased
+
+0.7.0 gave the agent a way to notice it was stuck and a way to ask. Running it against a
+real build showed that noticing is not the problem — **being told is.**
+
+Two models were given the same brief on Machine B, a React + Vite + Tailwind TODO app.
+`qwen3.5:4b` finished it in 88 minutes. `qwen3.5:0.8b` never wrote a single file. Both
+failures trace to the same missing thing, and the counted version of what follows is in
+[`doc/SESSION-ANALYSIS-0.7.0.md`](doc/SESSION-ANALYSIS-0.7.0.md).
+
+### Added — the agent keeps a record of what it already has
+
+Every anti-repetition device in the loop was a *sentence*: "You now know what is in the
+project", "Do NOT do it again". They all fired, correctly, and both models did it again
+anyway — because the sentence describes something the model can no longer see. It is
+asked to take the loop's word for what it is holding, and reaching for the tool is the
+cheaper way to be sure.
+
+`agent/workingSet` keeps the record instead of asserting it — paths read, written,
+listed and deleted, commands run, and what last went wrong — and renders it back on
+every turn. It runs off the step trace, so a 0.8B model gets the same footing
+`stepBrief` gives a 4B one without the 2B checklist threshold that excluded it.
+
+- On Tier B it is part of the prompt the loop rebuilds each turn.
+- On Tier A it is advisory and *moved* rather than appended, so exactly one copy exists
+ and it is always the current one, always adjacent to the decision.
+- A file the agent wrote counts as a file it has. The "write `App.jsx`, immediately read
+ `App.jsx` back" pair was a measurable share of the 4B session's 73 reads.
+
+### Changed — a repeated listing no longer ends the run
+
+Five of the 0.8B model's seven sessions died on the repeat guard, four at exactly two
+steps: `list_files`, `list_files`, `list_files`, session over. That is a read-only call
+costing five milliseconds, answered by ending the user's whole run — while a genuinely
+expensive mistake, a wrong `npm install`, gets a diagnosis and another go.
+
+A repeated **read-only** action now gets one substitution: the result it already had,
+handed back with the working set and an instruction naming the next move. Repeat after
+that and the guard ends the run exactly as before, because a model ignoring the content
+and the instruction together is stuck rather than disoriented. `write_file`,
+`run_script` and the rest are untouched.
+
+### Added — the step trace is a live panel that says why
+
+At 42 seconds a step the panel used to sit silent for minutes, and the user's first
+sight of a run going wrong was the summary at the end.
+
+- It opens on the first step and folds away when the turn ends — unless the user has
+ clicked it, after which we stop deciding for them.
+- Each row carries the model's own stated reason for the step, which both loops already
+ captured as `thought` and nothing rendered. Without it, eight reads of one file look
+ exactly like eight reads of eight.
+- `read_file` is gone from the panel in favour of *Reading*. The identifier belongs to
+ the tool protocol, not to the surface whose job is to explain the run.
+
+### Fixed — a compliment no longer restarts finished work
+
+The last message of the 4B session was *"It all works now, thank you"*. The agent
+answered it by building a checklist and starting to re-fix bugs it had already fixed,
+carried over from two turns earlier. The user cancelled the run.
+
+The gap was a category, not two words: **the user reporting that the work succeeded.**
+Adding `works` to the social vocabulary would be wrong, because "the delete button no
+longer works" is a bug report. So a success report is matched as a phrase, and any sign
+the sentence goes on to say something is still wrong — `but`, `still`, a negation,
+`almost` — hands it back to the agent. It is checked *after* the mutating-verb rule, so
+"it works now, can you also add a dark mode" stays a task.
+
+### Fixed — four kinds of bug report were being answered as greetings
+
+Found while fixing the above, and live on `main` until now. `isGreetingWithName` tested
+the first word against the whole of `SOCIAL_WORDS`, which is mostly filler — `it`,
+`the`, `got`, `all` — admitted there on the strength of a rule that only holds for whole
+messages. Read one word at a time, it made any message of three words or fewer a
+greeting:
+
+| Message | Was | Now |
+|---|---|---|
+| `it doesn't work` | chat | task |
+| `the tests fail` | chat | task |
+| `got an error` | chat | task |
+| `all buttons broken` | chat | task |
+
+Four dropped requests, which is the one outcome `intentRouter`'s header says it must
+never produce. Greetings now match a dedicated `GREETING_WORDS` set.
+
+### Documentation
+
+- `doc/SESSION-ANALYSIS-0.7.0.md` — the counted analysis of both evaluation sessions.
+- Two new marketing images, `live-session.png` and `knows-what-it-has.png`, with their
+ HTML sources; the hero and capabilities images are regenerated for 0.8.0.
+- The README and the hero image now say plainly that this is a **pre-release** installed
+ from GitHub Releases, rather than offering a Marketplace button that does not exist.
+
## [0.7.0] — unreleased
Everything here follows from one observation: a small model that is stuck does not know
diff --git a/README.md b/README.md
index ecdf32f..73d11c9 100644
--- a/README.md
+++ b/README.md
@@ -5,11 +5,17 @@
-
+
*A local Filipino-inspired AI coder that brings imagination and speed to your VS Code workflow.*
+> **Pre-release.** HirayaCoder is not on the VS Code Marketplace yet. Releases are
+> published as a `.vsix` on the
+> [Releases page](https://github.com/jaymar921/HirayaCoder/releases) and installed by
+> hand — [Step 4](#step-4--install-hirayacoder) has the one command it takes. Everything
+> described below works today; what is missing is the one-click install.
+
**HirayaCoder is a free AI coding assistant that runs entirely on your own computer.**
You type what you want in plain English, and it writes and edits the files for you — no
account, no subscription, no internet connection, and nothing you write ever leaves your
@@ -137,6 +143,36 @@ There is a longer, friendlier walkthrough in
+### Watching a run happen
+
+
+
+
+
+A local model can take the better part of a minute per step, so the panel shows you each
+one as it happens: what it is doing, which file, and the reason the model gave for it.
+It opens when the first step arrives and folds away when the turn ends — and if you open
+or close it yourself, it stays how you left it.
+
+That matters most when a run is going wrong. Six steps in, you can see it re-reading the
+same file or editing something you never asked about, and stop it — rather than finding
+out from the summary ten minutes later.
+
+### Small models that finish
+
+
+
+
+
+The classic failure of a very small model is not bad code — it is the same correct-looking
+action forever. HirayaCoder keeps its own record of every file the agent has read, written
+and deleted, every folder it has listed and every command it has run, and puts that record
+in front of the model on each turn.
+
+A repeated read is no longer fatal either. Asking twice for a directory listing used to end
+the run; now the agent is handed back what it already had, told what to do next, and only
+stopped if it asks a third time.
+
### The three modes
There is a row of buttons at the top of the chat. You can ignore them at first —
diff --git a/docs/images$name.png b/docs/images$name.png
new file mode 100644
index 0000000000000000000000000000000000000000..80c2a52858331be80901c53e1d8c4dd80a02c955
GIT binary patch
literal 71667
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