Skip to content

Improve script execution and error handling in builds v0.6.0 - #12

Merged
jaymar921 merged 10 commits into
mainfrom
feat/0.6.0-reliability
Aug 13, 2026
Merged

Improve script execution and error handling in builds v0.6.0#12
jaymar921 merged 10 commits into
mainfrom
feat/0.6.0-reliability

Conversation

@jaymar921

Copy link
Copy Markdown
Owner

This pull request introduces several improvements and new features to the agent session logic, focusing on better tracking of command execution, improved logging for debugging and evaluation, and enhanced checks for file existence. These changes support more robust session management and provide clearer insights into agent actions and outcomes.

Agent session and command tracking enhancements:

  • Added a commands array to the session summary, recording every command executed during a run (including cwd), so that a "done" cannot be accepted if the build failed during the session. This helps ensure that failures are not silently ignored in subsequent steps. [1] [2] [3] [4]
  • The recordCommand method and related data structures now support an optional cwd property, improving traceability of where commands were executed. [1] [2]

File existence checks:

  • Introduced the _existsInWorkspace method to safely check if a file exists at a workspace-relative path, using the same path confinement as other file operations. This is exposed to completion checks to validate that files named in tasks actually exist. [1] [2]

Logging and diagnostics improvements:

  • Added detailed logging for step transitions, including step number, attempt, and whether step-scoped or full request mode is used. This makes it easier to understand session progress and diagnose issues during evaluation.
  • Each agent action now logs a debug line with the action, target, outcome, and duration, providing a clear audit trail for debugging and user reports.

Session metadata enhancements:

  • Session and step records now include model parameter counts (params) for more granular reporting and analysis. [1] [2]

Dependency and ignore file update:

  • Updated .vscodeignore to always exclude node_modules/, clarifying that the extension has no runtime dependencies and preventing accidental packaging of development dependencies.

These changes collectively improve the reliability, observability, and maintainability of agent sessions and command execution tracking.

Commands started at the workspace root and nowhere else, so a project
scaffolded into a subfolder could never be built. The only phrasing models
had for it — `cd app && npm run build` — is refused as chaining, and the
refusal named no alternative, so they resent it until the repeat guard ended
the step. Across the v0.5.3 model round this was the single largest cause of
"failed to run scripts", and it had nothing to do with model size.

run_script now takes an optional cwd, resolved through the same pathGuard
confinement reads and writes use, and taken from the approved decision rather
than the request so it cannot be swapped in between the click and the spawn.
It is declared as an optional field in the action schema, without which
constrained decoding leaves Tier B models unable to emit it at all — the same
trap `recursive` was in. It joins the repeat-guard key, the step trace, and
the memory notes, so two installs in two folders are two actions, and a later
step builds where the earlier one did.

Also close stdin and set CI=1 on every spawn: `npm create vite@latest` asks
whether to proceed, and a process blocked on a question nobody will ever
answer was indistinguishable from a hung build until the timeout killed it.
A non-zero exit code is not a diagnosis. Handed `exit code 1` and 400 tokens
of npm output, small models resent the identical command, announced the build
had succeeded, or abandoned a task they were two steps from finishing — all
three seen in the v0.5.3 round.

Every failed run is now classified into one named reason — missing
dependency, wrong path, syntax error, permission, environment, network, port
in use — and each reason carries the one sentence the model can act on,
placed last in the observation so it is the final thing read before deciding.

Exactly two reasons are retried, once: a network blip and a file lock, where
the command was right and the world was briefly wrong. Nothing else is. The
damage in the testing round came from retries, not their absence, so a
missing dependency is told to install rather than quietly run again.

Servers no longer eat the whole budget. `npm run dev` succeeding looks
identical to `npm run dev` hanging, and every model asked to "confirm the dev
server starts" spent two minutes there and read the kill as a failure. Those
commands now get a 20-second probe: still up and quiet means it started, and
the model is told not to run it again. A server that printed EADDRINUSE or
could not resolve a module still fails, and says which.

Pre-flight answers the provable cases from the filesystem before anyone is
asked to approve anything: no package.json in that folder, no such script
(with the list of scripts that do exist), dependencies never installed. Each
costs a stat instead of a click, a subprocess, and a page of output.
Closing a chat tab called session.cancel(). That is the right answer for a
turn still queued and the wrong one for a turn in flight: on CPU inference a
turn is minutes long, so the agent is regularly mid-build when someone closes
the wrong tab, and everything it had done went in the bin at the moment it
could least afford it.

A running turn now detaches instead. The run continues headless — permission
prompts are VS Code modals rather than webview panels, so it can still ask —
and the transcript records the outcome, so reopening the session shows what
happened whether or not anyone was watching. A notification offers Reopen or
Stop, which is the explicit end the run does respect. A queued turn still
gives up its lane, because it has done nothing worth saving and holding the
lane starves every other session.

Reopening a detached session repaints the conversation and then says the turn
is still going, instead of showing an idle composer over a session that is
still writing files and refusing the next message with no visible reason.

The panel handlers move onto the tab, since a detached session opens a second
panel when it is reopened and the registry wiring was attached only to the
first — the tab would never have been forgotten and focus would have stopped
following it. Closing the window still stops everything: the extension host
is going away, and an orphaned npm install has nothing left to read it.
…mbers in front of you

Three things that all come back to a small model losing the thread.

The goal is now restated last, immediately before the instruction to act.
contextBuilder already puts the task at the top of a block rebuilt every turn,
which is correct and is not enough: by the time a 1B model has read a project
overview, a file listing, session memory, a step trace and 400 tokens of npm
output, the sentence saying what it is for is thousands of tokens behind it,
and recency wins. The step count rides along, because "you are on step 6 of 8"
is what turns "keep exploring" into "write the file now". Sixty tokens a turn,
the cheapest thing in the prompt.

A `done` is now challenged when the task names three or more files and some of
them do not exist. Three paths is a structure being specified rather than a
sentence that happens to mention a file, and the benchmark shape — eleven
files asked for, four written, "done" — was invisible to every other check.
Existence is tested against the workspace, not just against what the run
wrote, so files that `npm create vite` produced count as produced.

The ledger records parameter count alongside the timings it has always kept,
and the adaptation report finally shows both: average and slowest turn, time
per action, and what share went to waiting on the model, ordered smallest
model first. `timings()` has existed since 0.5.0 and was called from nowhere,
so "gemma4:e4b took about thirty minutes" was a thing you could only learn
with a stopwatch. Failed commands now carry their diagnosed reason as the
error code, so the failure counters say MISSING_DEPENDENCY rather than 37.
…behind a slow run

Logging fills the three gaps that made the v0.5.3 round hard to review after the
fact: both ends of every TODO item rather than only the closing one, the step
transition with its budget and whether the experimental brief was in use, and
one debug line per action with its outcome and duration — the record that
settles "it said it edited the file and it did not". The translator now logs the
note it stored, not only the ones it threw away; a stored note comes back as
trusted context on every later turn, and it was the one decision going
unrecorded.

CHANGELOG and FEATURES cover the round: the workspace-root limit on commands,
the scaffolder prompt that looked like a hang, failure reasons and the two
retryable ones, the dev-server probe, pre-flight checks, tab close no longer
killing a run, the goal restated at the decision point, the named-file
completion check, and the performance comparison in Show Learned Adaptation.

0.5.3 is left as its own unreleased section, since it is the build the round
was run against and its notes are the record of what that build got wrong.
The 0.6.0 build packaged at 22.69 MB against 0.37 MB for 0.5.3 — 3,749 files
of React tooling, none of which this extension has ever imported. It is the
same root cause as the rest of this release: the model round could not run a
command anywhere but the workspace root, so `npm install` for the TODO app
went into the extension's own tree. package.json had already been cleaned back
to `"dependencies": {}`; the packages themselves were still sitting in
node_modules, and vsce packaged what it found.

`npm prune` is the fix. Excluding node_modules outright is the guarantee: with
no runtime dependencies there is nothing under it that belongs in a shipped
build, and a stray install should cost a rebuild rather than a release. The
line carries the warning that adding a real runtime dependency means removing
it. Repackaged: 84 files, 394.9 KB.

The lock file picks up the version bump.
Caught on `gemma3:1b` in a live build run. It ran `node src/main.js` before it
had written main.js, Node answered

  Error: Cannot find module 'C:\…\hiraya-build\src\main.js'

and the new diagnosis told it to run npm install — for a file it had simply
not created yet. The model went and read a different file instead, and the
phase ended on the repeat guard.

Node reports a missing file and a missing package with the same four words.
The rule tried to separate them by excluding a leading dot or slash, which a
Windows absolute path sails straight past on its drive letter. Both cases are
now decided on the specifier: a separator anywhere means a path the code
named, and only a bare word — or a bare @scope/name — is a package. Vite's
"Failed to resolve import './Foo'" moves across for the same reason, and
MODULE_NOT_FOUND joins the path rule as a backstop.

The wrong answer was worse than no answer here: it sent a model that was one
write_file from working towards an install it did not need.
From a live 0.6.0 run of `gemma4:e4b` on the TODO-app spec. It installed,
scaffolded, and wrote every component correctly — the cwd fix worked, npm ran
where it was supposed to — then `npm run dev` died in 1.3 seconds:

  VITE v5.4.21  ready in 2912 ms
  [Failed to load PostCSS config: [ReferenceError] module is not defined in
   ES module scope … at postcss.config.js:1:1]

`npm create vite` writes "type": "module"; the model wrote postcss.config.js
with `module.exports`. A two-line fix. Instead the run reported the app
finished, because nothing told it otherwise.

Three gaps, all of them ours:

**No rule matched.** A ReferenceError is not a SyntaxError, so `diagnose`
returned null and the model got forty lines of Vite stack trace with no
sentence saying what to do. The ESM/CJS mismatch — the single most common way
a Vite + Tailwind scaffold fails — now has its own reason, naming both repairs
(`export default`, or the same content as `.cjs`).

**A server that exits still read as unexplained.** Vite prints "ready in
2912 ms" and *then* dies, so the most recent line in the output is the one
saying it worked, which is exactly what a model believes. Any long-running
command that exits at all is now a failure that says so, whatever it printed
on the way out.

**The loop offered the wrong next move.** For a failure in a file the model
itself wrote, "use a different action, or finish with done" talks straight
over an observation that has already named the file and the fix. Diagnoses
carry `fixFirst` now, and those get told to open the file, correct it, and run
the same command again — not to start something else and not to finish.

And the structural half, which does not depend on the model reading anything:
a `done` is challenged once while the last command it ran is still failing. A
project whose build does not run is not finished. A run that broke the build
and then fixed it passes untouched — that is the behaviour being asked for.
…ing a broken server

Three findings from a live qwen3.5:4b run of the TODO-app spec, all of them
ours.

**npm installed outside the workspace.** The workspace was
.ignore/0.6.0-todo-app-qwen3.5-4b, the project was in todo-glass-app/ inside
it, and every npm command carried the right cwd except one:

  13:52:02  "command":"npm install --save lucide-react"   ← no cwd

There is no package.json at that workspace root, and npm searches *upwards*
until it finds one. It left the workspace, left .ignore/, and installed the
dependency into this extension's own package.json. Exit code 0, reported as a
success, three directories outside anything the user had opened — and it is
where the tailwind/lucide pollution in this repo came from, twice.

Path confinement binds the agent's tools; a subprocess resolves paths however
it likes, and nothing here sees npm's search happen. Pre-flight now performs
that search itself, bounded at the workspace root, and refuses any
package-manager command whose manifest would resolve outside — naming what to
set cwd to instead. A manifest in a parent folder that is still inside the
workspace, as in a monorepo, is allowed: that one is the user's own.

**A dev server serving nothing but 500s counted as started.** The probe asked
"did it fall over?" against a list of specific failures — a port collision, a
missing module, a config that would not load. Vite starts fine with a broken
PostCSS config, announces "ready in 906 ms", and then fails every request. No
pattern matched, so the probe reported a working server. The test is now the
general one: did it say "error". A clean start does not, and "0 errors" is
excluded so a build announcing its own success is not misread.

**An error nobody wrote a rule for.** A rule list only covers what someone has
already seen. Tailwind 4 moving its PostCSS plugin to @tailwindcss/postcss
gets its own rule, since it fires on essentially every Vite + Tailwind
scaffold a model writes. Behind it sits the general answer: nearly every build
error names the file it choked on, so when nothing matches, the first project
file in the output is extracted — skipping the frames that run through
node_modules — and the model is told to open that file, change what the error
describes, and run the command again. No classification needed.

The old pre-flight test asserted that `npm install` with no manifest in scope
was fine. That assertion encoded the bug and is now the escape suite.
Five models against the build-a-TODO-app benchmark, JavaScript only. The three
at or below 1.2B score 0/3 — qwen3.5:0.8b and gemma3:1b never write both files
and end on the repeat guard, llama3.2:1b creates a *folder* called
src/todo-app.js and tries to run it. qwen3.5:2b passes all three phases in 35
minutes, which is the first full pass this benchmark has recorded from a model
this small.

Recorded as-is. The 0.5.3 baseline these should be read against has not been
run yet, so none of this is an improvement claim — and the two script fixes
that landed after them (1be0524, 3d4d370) are not in these numbers either.
@jaymar921 jaymar921 self-assigned this Aug 13, 2026
@jaymar921 jaymar921 added enhancement New feature or request fix Bug fixes labels Aug 13, 2026
@jaymar921
jaymar921 merged commit 2e2eea7 into main Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request fix Bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant