Skip to content

Release v0.5.0 with bug fixes, performance improvements, and docs update - #10

Merged
jaymar921 merged 16 commits into
mainfrom
feat/step-sessions-enhanced-memory
Aug 13, 2026
Merged

Release v0.5.0 with bug fixes, performance improvements, and docs update#10
jaymar921 merged 16 commits into
mainfrom
feat/step-sessions-enhanced-memory

Conversation

@jaymar921

Copy link
Copy Markdown
Owner

This pull request introduces several important updates and experimental features to the agent's session handling and evaluation logic, with a focus on improving the accuracy of step completion detection, adding support for experimental "step sessions," and enhancing memory and import handling. The most significant changes include fixing a critical bug in how file changes are tracked, supporting more granular and robust step execution, and updating the changelog with detailed explanations of these improvements.

Core logic and bug fixes:

  • The ChangeSet class now tracks a monotonic revision counter instead of relying on the number of unique files, ensuring that edits to files created in earlier steps are properly recognized as changes. This fixes a major issue where important edits could be missed and incorrectly reported as failures. (app/agent/agentSession.js) [1] [2]
  • The judgeItem function now uses this revision counter to determine if a step made progress, rather than comparing the number of files before and after. (app/agent/agentSession.js)

Step sessions (experimental feature):

  • Introduced an experimental "step sessions" mode, controlled by this.stepSessions, which:
    • Runs each TODO item as its own session with a brief and a retry mechanism.
    • Checks each step's completion against its own text and allows for a single retry before stopping the run if the step fails again.
    • Rebuilds the context and memory recall per step, improving relevance of recalled information. (app/agent/agentSession.js) [1] [2]

Memory and context improvements:

  • Memory recall is now subject-aware, not just recency-based, so steps get relevant notes even if they are not the most recent. (app/agent/agentSession.js)

Import and file handling:

  • When reading files, local imports are now resolved and included, up to a defined limit, to provide better context for the agent and avoid unnecessary repeated reads. (CHANGELOG.md)
  • File import paths are now checked for correctness, including case sensitivity, and warnings are appended when an import cannot be resolved, helping to catch subtle bugs that could break builds on some platforms. (CHANGELOG.md)

Documentation and benchmarking:

  • The CHANGELOG.md has been updated with thorough explanations of all the above changes, including benchmarks, measured outcomes, and rationale for experimental features. (CHANGELOG.md)

These changes collectively improve the reliability, transparency, and debuggability of the agent's step execution and evaluation process.

jaymar921 and others added 16 commits August 13, 2026 08:04
From the React + Vite + Tailwind evaluation in .ignore/2.react-todo: five
models, five sessions, and src/App.jsx ended every one of them still holding
Vite's scaffolded counter demo. Components got written; nothing was ever wired
to anything. Almost none of it was the models.

Three shipped bugs, each of which silently degraded a whole feature:

judgeItem compared changeSet.size() across a TODO item. A ChangeSet is keyed by
path, so an item that edits a file an earlier item created leaves the map the
same size — the item was judged to have changed nothing, its done was
challenged, and a step that wrote a real file was reported as "it asked for a
file and none was written". Scaffold-then-assemble is the shape of every plan
worth making, so the item doing the work the user cared about was the one most
likely to be scored a failure. ChangeSet now carries a monotonic revision.

The completion check only challenges a done that changed nothing when the text
"requires a change", decided by a verb list written against messages people
type — and applied to text the planner writes. qwen3.5:4b planned "Assemble
App.jsx layout..." and "Configure exact folder structure...", neither verb was
listed, and both items passed unchallenged as done (no files changed).
requiresChange now takes { planned: true } with a separate vocabulary, kept
separate because "explain how the router handles a request" must still finish
having written nothing.

Challenged on a done, both qwen3.5:4b and ornith:9b replied by asking the user
what to work on, with the request still in the first message of the same
conversation. The objection now restates the ask verbatim.

Added — a read carries what the file imports. Reading App.jsx told a model a
hook was imported and nothing about what it returned, costing one turn per
import at CPU speed; qwen3.5:4b spent all 44 of its steps reading and listing
and wrote nothing. core/importGraph resolves local specifiers and read_file
includes them, capped, behind the same permission gate.

Added — step sessions, experimental and off by default, behind a header toggle
and hirayacoder.experimental.stepSessions. Each item is run as a briefed step
(agent/stepBrief) showing what the earlier steps actually wrote rather than only
that they finished; each is checked against its own text before it may close
(agent/stepGuard), since gemma2:latest edited vite.config.js and README.md while
working a list about useTodos, TodoInput and App.jsx; and a step that fails gets
one retry with the diagnosis stated, then stops the run with a workaround rather
than cascading through steps that depend on it.

Added — MemoryStore.readRelevant selects notes by subject before filling the
window by recency, path-aware so an item saying useTodos finds a note saying
src/hooks/useTodos.js. The step that had to assemble App.jsx ran sixth, by which
point the notes it needed were the first to fall out of the window.

Added — tools/bench-steps.js, the live benchmark for this failure. Neither
existing harness reproduces a project that already exists plus a multi-item
request whose last item must import what the earlier ones wrote. It resolves the
resulting imports rather than trusting that App.jsx names them.

Verified live on qwen3.5:4b, the model that failed before: 4 of 4 items,
App.jsx rewritten and importing all three files the run built.
Correcting the previous commit's closing claim. It said the live qwen3.5:4b run
had App.jsx "importing all three files the run built". That was measured with a
grader that only checked App.jsx *named* those imports on an import line, which
is too weak by exactly the margin that matters. Re-run with the specifiers
actually resolved, the same model wrote, from inside src/App.jsx:

    import { useTodos } from '../hooks/useTodos.js';
    import { TodoInput } from '../components/TodoInput.jsx';

Both climb one level too many, and TodoList was not imported at all. The right
files, the wrong route. The app does not build.

What the previous commit did fix stands: App.jsx is now rewritten at all, with
the counter demo gone, where five models across five sessions never touched it.
This is a narrower failure that was hiding behind that one.

Every guard passed it — the file is large, its brackets balance, it exports, no
body is a placeholder, the change set grew, and the file the step named is the
file that changed. Nothing in the project could see it.

importGraph.brokenImports resolves a written file's relative specifiers; bare
packages are excluded, since whether react is installed is a question about
node_modules rather than about what the model wrote. write_file appends the
failure to its own observation with the corrected path where exactly one file in
the workspace carries that name, and records it in detail.brokenImports.

Appended rather than refused, deliberately. The content is otherwise fine and
discarding a whole file over a path is where truncation and placeholder bodies
come from on a small model. stepGuard then reads the recorded result — no second
trip to disk, and no disagreement with what the model was told at the time — and
fails the step so the retry fires, with an instruction to correct the paths
rather than start over. Only the newest write per path counts, so a step that
wrote a broken file and then fixed it is not failed for the draft.

bench-steps.js now resolves the imports it grades and prints them verbatim, so
"attempted the wiring and got the path wrong" no longer reads as success.
createClient takes timeoutMs; requestTimeoutMs is the *setting* name and is
silently ignored, so the harness ran at the 300s default rather than the 600s
it asked for. On qwen3.5:4b that timed out mid-generation while rewriting
App.jsx with four imports, and the step survived only because the retry caught
it. Raised to 900s so the benchmark measures the model rather than the clock.
The React/Vite evaluation and all four bench-steps runs were produced on
Machine A - the CPU-only Lenovo laptop that doc/MODELS.md calls the design
constraint. Nothing said so, which makes the timings uncomparable and the
conclusions weaker than they are: every model failing the same way matters more
on the machine the project is shaped around than it would on a fast one.

bench-steps.js now requires --machine, as bench-build.js already does. This task
runs 20+ minutes on A and a fraction of that on C, so a result filed without its
machine cannot be compared with anything.

Recorded alongside it: the shipped hirayacoder.ollama.requestTimeoutMs default of
300000 is not enough on Machine A to generate one App.jsx with four imports. It
timed out mid-run on qwen3.5:4b. That is an ordinary file on the machine this
project exists for, so it is documented in doc/MODELS.md with a recommendation to
raise it for laptop work. The default itself is left alone - it is also what makes
a genuinely hung request noticeable on a fast machine, and B and C never approach
it - so this is a documentation change, not a behaviour change.

Run 3 is worth noting for the opposite reason: it completed only because that
timeout produced a step that wrote nothing, the guard failed it, and the retry
ran it again and succeeded. Under 0.4.0 the item would have ended empty. A slow
machine and a step retry interact well.
Machine A cannot run the control. Its comparison is 0.4.0 code against 0.5.0
code, and those differ by three bug fixes as well as by step sessions - so it
supports '0.5.0 wires the app where 0.4.0 never did' and not 'step sessions are
what did it'. At 20+ minutes a run the paired steps/nosteps control was
unaffordable; on Machine B it should be minutes.

The handoff names the two things Machine A left open. The control itself, and
whether the broken-import guard ever fires live - it has twelve unit tests built
from Machine A's verbatim output, but no live run has produced a bad path since
it was added, so it is unit-tested and not yet observed working.

It also asks Machine B to settle whether the 300000 request-timeout default is
only a laptop problem, and says plainly that 'both succeed' would be a more
useful answer than a confirmation, so the run is not read as looking for one.
… Linux

Found while writing the Machine C handoff, then confirmed on Machine A rather
than handed off. importGraph resolved a written file's imports with fs.stat,
which on Windows and macOS resolves ./hooks/usetodos.js to useTodos.js and
reports success. So a model that gets the case wrong produces a file that builds
locally and fails on Linux CI or a Linux deploy, and the guard added one commit
ago would have called it fine - wrong in the one direction that ships a broken
build.

existsExactly reads each parent directory and compares every path segment
byte-for-byte, because readdir returns the real spelling however the lookup was
cased. Directories count as well as filenames: ./Hooks/useTodos.js is as broken
on Linux as ./hooks/usetodos.js. findByStem still matches case-insensitively,
which is what lets the suggestion carry the correct spelling back.

Also records Machine A run 4: App.jsx rewritten, counter gone, all three imports
resolving, 781s. Three of four runs now produce a wired app - but that is a rate
from four samples, and the one failure predates the import guard, so it cannot
be read as evidence the guard fixed anything. The guard still has not been
observed firing live.

Adds setup/FOLLOWUP-PROMPT-MACHINE-C.md. Machine C's job is the repeat count
neither A nor B can afford: a success rate, and enough runs that the guard has a
real chance to fire.
Machines B and C are benchmarking concurrently on this branch. bench-steps only
printed to stdout, so both would have hand-edited the same region of
doc/MODELS.md - the exact conflict bench-build's one-file-per-run rule exists to
prevent. Same convention now: benchmarks/results/<machine>/, never appended to a
shared file.

The record carries the grade from the filesystem, the App.jsx import lines
verbatim, and the model's own summary kept and clearly labelled as graded on
nothing.

tools/bench-steps-summary.js collates them into a rate. This matters more here
than on the other benchmarks: one run of the wiring task tells you very little -
Machine A ran the same model against the same fixture four times and got three
different outcomes - and Machine C is being asked for five to ten repeats, which
is a lot of terminal scrollback to collate by hand.

It names each failing run and why rather than only reporting a percentage. A
bimodal result is the interesting kind and averaging it away is the mistake both
handoffs warn against.

Verified end to end on qwen3.5:0.8b, which fails the task in under a minute and
is therefore a cheap way to exercise the writer.
The README opened with "fully offline VS Code extension that pairs your editor
with a local Ollama LLM", then went straight into tier tables, VRAM ceilings and
a nineteen-run benchmark matrix. All of it true, none of it readable by the
audience most likely to want a free private coding assistant: someone learning
to code, or not coding at all yet.

Restructured so the first half answers the questions a beginner actually has, in
the order they have them. What is this. How is it different from ChatGPT. Will it
run on my computer - answered by RAM, with instructions for checking how much you
have. How do I install it, in four steps with what each one is for. What do I
type first, with examples that work.

Then a troubleshooting section written around what small models really do, since
those failures look like the user's fault and are not. A refused write is the
guards working and says so. "It said done but nothing changed" is a named habit
with a fix. "It's taking forever" is a RAM problem with a smaller model as the
answer.

The honesty is deliberate and load-bearing: there is a comparison table that says
outright that quality is "good, not great" against the paid tools, and a
paragraph saying a request for a whole social media app will disappoint. Someone
who expects Copilot and gets a small local model will otherwise conclude they did
something wrong.

Nothing was dropped. The constraints, benchmark tables, machine specs and
recommendations moved under "For developers" or into doc/MODELS.md, which already
held the full detail. Every factual claim was checked against the source rather
than carried over: the keybinding, the VS Code floor, zero production
dependencies, the Review diff button, deletes confirming even under Auto Edit,
and the allow-listed binaries.
Machine A could afford four runs of this task and got three different outcomes,
so it asked C for the thing four samples cannot give: a rate. This machine runs
it in 30-65s, so 25 runs cost under half an hour.

Ten pairs of qwen3.5:4b plus five gemma4:e4b, one at a time with nothing else
running, every file written by the harness and committed unedited. All 25
reported 100% GPU, gemma4:e4b at 9.6 GB included.

The two qwen3.5:4b failures are kept for the reason the handoff gives for
keeping them: one wired every import correctly and left the counter demo in
place while reporting 4 of 4 done, and one wrote every import a level too high
and triggered the first live firing of the broken-import guard.
…n firing

Answers the five questions in the Machine C handoff from the 25 runs in the
previous commit, and adds a subsection alongside A and B rather than touching
either.

The headline is a negative result: 80% with step sessions against 70% without,
at n=10 each, is indistinguishable. What the comparison does separate is 0.5.0
from 0.4.0 - where Machine A saw App.jsx untouched in five sessions out of five,
the same nosteps path wires it 7 times in 10. The three bug fixes did the work.

Also records that bench-steps-summary.js overstates the nosteps rate as 100%.
Its predicate is wired.length > 0, so one resolving import out of three passes,
against its own comment saying two out of three is a broken app. Three nosteps
runs wired only part of the app - two never imported the hook holding the state.
Reported rather than patched: Machine B is collating with the same tool right
now, and moving a grading bar after seeing results is what section 5 forbids.

The broken-import guard fired in a live run for the first time, reproducing
Machine A's run 2. The retry did not rescue it - the model re-read all four
files and wrote the same wrong paths - but the run ended partial and said "3 of
4 completed" instead of the confident 4 of 4 it gives elsewhere.

Confirms the macOS half that had only ever run on Windows: the case tests pass
on APFS, and the workspace boundary refuses traversal spelled with
differently-cased components. Full suite 932 passing, 0 failing, so Machine A's
four failures do not reproduce off OneDrive.
package.json is at 0.5.0 and package-lock.json still said 0.4.0, so npm install
rewrote it. Committed on its own rather than folded into the benchmark commits.
Add Machine C results and update documentation for benchmarks
Machine C caught this by hand-counting 25 runs. The predicate shipped as
(g.wired || []).length > 0 while its own comment claimed 'two out of three is a
broken app that reports itself as finished'. The code won.

Three nosteps runs had wired only part of the app - two imported TodoInput and
TodoList and never useTodos, so the hook holding all the state was written and
unused; one imported only useTodos, so neither component rendered. None had a
broken path, only missing ones, so broken was empty and the check passed. A real
7/10 was reported as 100%.

It failed in the worst possible direction. The inflation landed on the control
arm, which is the one that makes the feature under test look bad by comparison,
and the reading it produced was 'step sessions made things worse' from 25 runs
that say no such thing. Corrected: 8/10 with step sessions against 7/10 without,
which is indistinguishable at n=10.

The bar is now what the comment always said, and it travels in each record as
graded.expected so a future collator cannot quietly disagree with the task it is
grading. Partially-wired runs get their own column instead of hiding among the
passes - 'imported two of three' and 'imported none' are different problems.

benchStepsSummary.test.js pins every shape those 25 runs produced, including the
two that used to slip through, and asserts the collator reproduces Machine C's
hand count. The module now guards its main behind require.main, like
bench-build.js, so requiring it to test the grading half does not run a sweep.

Re-grading after the fact is safe because the per-run JSON is the source of
truth: nothing needs re-running, and Machine B - benchmarking right now - only
has to pull and regenerate its table.
…oad data

Two sweeps of four paired models, 16 runs, one at a time with nothing else
running. The table is the second sweep on the tree of c38c424; the first ran on
7c8ab04, before the case-sensitivity fix, and is kept as a repeat rather than
merged into the rate.

The control Machine A could not afford says nosteps does not fail where steps
succeeds. It never failed at all: all 12 Tier A runs rewrote App.jsx and removed
the counter demo either way, and Machine A's motivating failure - components
written, App.jsx untouched in five sessions out of five - did not reproduce
once. Same conclusion Machine C reached from a distribution, reached
independently on different hardware. The three bug fixes did the work.

The pairs cannot separate the two arms and the second sweep proves rather than
suggests it: two cells changed answer between sweeps with nothing altered but
the run. Counting all 12 Tier A runs, nosteps is fully wired 5 in 6 and steps 3
in 6 - pointing opposite to C's 8/10 against 7/10 and equally insignificant
(p ~ 0.55). Two machines disagreeing in direction at n~6-10 is what no effect
looks like.

What the pairs do separate is cost. nosteps was faster in all eight pairs, mean
255.0s against 297.6s: step sessions cost ~17% wall clock, and eight out of
eight in one direction is a real signal where the correctness difference is not.

The broken-import guard did not fire in 16 runs, and the reason is informative.
Machine B produced zero broken paths - every miss was a missing import, models
writing useTodos.js and then importing useState instead, leaving the hook
holding all the state written and unused. The guard needs a path pointing at
nothing; C saw exactly that and saw it fire. The failure has two modes and only
one is guardable. The truncation and export-preservation guards did fire here.

The retry never fired on B at all, 0 in 16. Its value rests on C's evidence.

gemma4:e4b is the most interesting row: fully wired 3 of 4, indistinguishable
from qwen3.5:4b, at 85% CPU / 15% GPU with 9.5 GB resident against a 4 GB card -
and only 4% slower for a model 2.8x the size. A long generation is bottlenecked
on the CPU-resident layers either way, so a badly-split model is not unusable
on this task.

No request timed out in any run; whole-session times were 211-348s and a session
is many requests. Three machines now agree the 300s default is Machine A's
problem alone.

Suite is 947 passing, 1 failing. The 4 transcriptStore/scriptRunner timeouts
predicted from Machine A do not reproduce, which with C's identical result
confirms them as A's OneDrive-synced working directory. The one real failure
arrived with the README rewrite in c38c424: the rewritten Requirements section
says "(node, npm, python, git, and similar)", dropping gradle, make, java and
javac from DEFAULT_ALLOWED_BINARIES, and the drift test catches it. Left
unfixed - section 5 forbids code changes inside a benchmark run.
Closes the release with what three machines actually found, including the part
that does not flatter it.

Step sessions do not improve correctness. 33 paired runs of the wiring benchmark:
Machine C 8/10 with against 7/10 without (p = 1.0), Machine B 3/6 against 5/6
(p = 0.55). Two machines disagreeing in direction, neither significantly, is what
no effect looks like. The cost is the part that is significant - B's nosteps arm
was faster in all eight pairs, about 17% less wall clock (sign test p = 0.008),
exactly what an extra planning call plus one loop per item should cost.

So the three bug fixes are what fixed the original failure, not the feature built
on top of them. v0.4.0 left App.jsx untouched in five sessions of five; the
control arm of these runs wires it 7 of 10 on C and 5 of 6 on B, and Machine A's
motivating failure never reproduced on B.

The feature ships off by default and labelled experimental, because it buys
honesty rather than capability: a step checked against its own text, and a run
that stops and explains itself instead of cascading. Machine C watched exactly
that - the import guard fired on a real wrong path, the retry failed, and the run
ended '3 of 4 completed' with the reason attached, where the same model unguarded
reported '4 of 4' for an app that still had the counter demo in it. FEATURES.md
now says outright to leave it off unless you want the stricter reporting.

Also fixes a README regression the rewrite introduced and Machine B caught:
runScriptTool asserts every DEFAULT_ALLOWED_BINARIES entry appears in the README,
and the beginner rewrite replaced the toolchain table with 'node, npm, python,
git, and similar', dropping java, javac, mvn, gradle and make. The full list is
back, in the troubleshooting section where a beginner looks for it.

943 unit tests passing. The one failure is transcriptStore timing out on this
machine's OneDrive-synced directory; B and C both confirmed it does not reproduce
on their hardware.
@jaymar921 jaymar921 self-assigned this Aug 13, 2026
@jaymar921 jaymar921 added documentation Improvements or additions to documentation enhancement New feature or request fix Bug fixes labels Aug 13, 2026
@jaymar921
jaymar921 merged commit a4fa06d into main Aug 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request fix Bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants