feat(core): guarantee terminal output files and add an agent-friendly summary output style - #36703
Draft
AgentEnder wants to merge 3 commits into
Draft
feat(core): guarantee terminal output files and add an agent-friendly summary output style#36703AgentEnder wants to merge 3 commits into
AgentEnder wants to merge 3 commits into
Conversation
Contributor
|
View your CI Pipeline Execution ↗ for commit 55ddce4
☁️ Nx Cloud last updated this comment at |
AgentEnder
force-pushed
the
feat/nxc-4694-4698-agent-output
branch
4 times, most recently
from
August 21, 2026 16:48
5eabbf2 to
b69a54e
Compare
Every task that reaches a terminal state now writes its terminal output to `<cacheDir>/terminalOutputs/<hash>`, whatever its `cache` setting, output style, or batch membership. That path is what the static output styles point users and agents at, and what Nx Cloud reads as a fallback when a task's in-memory output is undefined, so a task finishing without a file there was a dangling reference. The gaps were: - Batch tasks. Their output only ever arrives over IPC, so the sole thing that ever put it on disk was `cache.put`. A `cache:false` batch task — the shape CI runs under `NX_BATCH_MODE` — left nothing behind at all. - Failed cacheable tasks. Failures aren't cached (without NX_CACHE_FAILURES), so nothing wrote them either — exactly the output an agent wants. - Streamed `nx:run-commands` tasks run directly, whose write was gated on the same condition as the printing. - Tasks whose fork threw before a process existed. `postRunSteps` — the one place every discrete and batch result passes through — now backstops all of them. On the double write with `cache.put`: skip-if-same, resolved by construction rather than by comparison. The set of results `cache.put` will write is now computed up front (`resultsToCache`) and handed to the backstop, which skips exactly those tasks. Making `put` reuse an already-written file would have meant changing the napi `NxCache.put` signature, which Nx Cloud calls directly. Cache replays are skipped for the same reason — the output they replayed was read from that very file. Tasks whose runner already owns the file (forked processes write it as they exit; a continuous task's exit is its only chance) are recorded in `tasksWithPersistedOutput` and skipped too, so each task's output is written once. That also answers the old TODO on the run-commands exit handler: the disk write is what a continuous task needs, the printing is not. What lifecycles receive is unchanged — `TaskResult.terminalOutput` is still populated for every status. This changes only what lands on disk.
`remove_old_cache_records` only ever walks hashes it finds in
`cache_outputs`, and only `put` writes rows there. Terminal outputs written
without a cache entry — uncacheable tasks, and cacheable ones run with
`--skip-nx-cache` — therefore had nothing pointing the GC at them and
accumulated in `<cacheDir>/terminalOutputs` forever. Guaranteeing those files
exist made that leak worse, so it is fixed here.
`cache_outputs` gains `has_artifacts`, and `NxCache.record_terminal_outputs`
registers a batch of hashes with `has_artifacts = 0`. The existing 7-day sweep
then collects them with no new deletion logic — it already unlinks both the
`<hash>` directory and the terminal output file.
The flag is what keeps a bookkeeping row from being replayed as a cache hit.
`get` and `fetch_cache_rows` filter on it, because a row alone used to mean
"cache hit": without the filter, a `--skip-nx-cache` run (whose hash is
unchanged by the flag, and which writes no artifacts) would leave a row that
the next ordinary run reports as a hit while restoring nothing. `put` forces
the flag back to 1 on conflict so a real entry always supersedes a
bookkeeping row, and the bookkeeping insert touches only `accessed_at` on
conflict so it can neither demote a real entry nor overwrite the size that
covers its artifacts. Updating `accessed_at` also keeps a task that is still
run daily from aging out and having its current output deleted underneath it.
Continuous tasks are registered as they exit, since they never reach
postRunSteps and their file is written by the runner.
Two adjacent fixes fall out of the same invariant:
- `ensure_cache_size_within_limit` deleted the row and the `<hash>` directory
but not the terminal output file, stranding it with nothing left to point
the GC at it. It now removes both, matching `remove_old_cache_records`.
- `check_cache_fs_in_sync` asks whether any cache records exist; that now
means records with artifacts, since only those own a directory that can be
out of sync with the filesystem.
DB_VERSION is bumped rather than migrating: the db path is version-scoped
(`{name}-v{DB_VERSION}.db`) and stale versions are reaped, so a bump gives a
fresh schema with no ALTER TABLE.
…rinting them An agent driving nx gets the static renderer and reads thousands of lines of passing output to find the one failure. Since NXC-4694 every task leaves its output at `<cacheDir>/terminalOutputs/<hash>`, so the run itself no longer has to carry that text. `--output-style=summary` prints run counts and one line per failing task naming the file holding its output: NX 34 tasks: 31 succeeded, 29 cached, 1 failed, 2 skipped x nx run js:test (exit 3) full log: /repo/.nx/cache/terminalOutputs/9f2c... Re-run with --output-style=static to inline logs. Nothing is printed for a task that succeeded, and no task output is inlined at all, so the size of a run's output depends on how many tasks failed and never on how much they logged. Inlining a fixed tail was considered and dropped: every consumer of this style can read the file - it is chosen for agents or asked for explicitly - and a fixed slice is as likely to catch a runner's footer as its error. The style that inlines logs is named in the closing line. It is the default when `isAiAgent()` and no style was given. That resolution sits in the existing yargs middleware chain, after NX_DEFAULT_OUTPUT_STYLE and before the TUI check, so an explicitly requested style always wins and the TUI correctly declines. Deciding it at life cycle selection instead would have skipped NX_DEFAULT_OUTPUT_STYLE and landed after `--output-style` was normalized. `terminalOutputPathForHash` is shared from `tasks-runner/cache` so the renderer can address a log without holding a cache instance; it mirrors the native layout, which the legacy cache already agreed with.
AgentEnder
force-pushed
the
feat/nxc-4694-4698-agent-output
branch
from
August 21, 2026 18:15
b69a54e to
55ddce4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Stacked on #36453 (base is
fix-nxc-4693-4692, notmaster). Review the three commits abovefb9627ee. I will retarget tomasteronce #36453 merges.Two tickets, in one PR at Jason's request — the second is meaningless without the first.
Current Behavior
Terminal output files are not guaranteed. The
writeFileSyncthat persists task output lives only in the non-streaming single-task branch. Batch output arrives over IPC in memory and only lands on disk viacache.put, which is gated ontask.cache. So acache:falsebatch task — the main CI path underNX_BATCH_MODE— leaves no terminal output file, and anything pointing at<cacheDir>/terminalOutputs/<hash>dangles exactly there.Agents read transcripts to find one failure. With the TUI off they get the static renderer and thousands of lines of passing output.
Expected Behavior
NXC-4694 — every task leaves its output on disk
Every task that reaches a terminal state writes to
<cacheDir>/terminalOutputs/<hash>, regardless ofcachesetting, streaming mode, or batch membership. No double-write whencache.putalso stores it.Outputs written without a cache entry (uncacheable tasks, and cacheable ones under
--skip-nx-cache) get a row via a new napirecord_terminal_outputs, carryinghas_artifacts = 0. Without a row the file is invisible toremove_old_cache_records, which only walks hashes it finds in the database, so these files would accumulate forever. The flag means such a row can never be served as a cache hit — there are no artifacts behind it.Bonus interop: Nx Cloud's upload fallback already reads this exact path when in-memory output is undefined, so guaranteed files make that path real for streamed and continuous tasks.
NXC-4698 —
--output-style=summaryBecause the files are now guaranteed, a run no longer has to carry that text:
Nothing is printed for a successful task, and no task output is inlined at all — so the size of a run's output depends on how many tasks failed, never on how much they logged.
It is the default when
isAiAgent()and no style was given. That resolution sits in the existing yargs middleware chain, afterNX_DEFAULT_OUTPUT_STYLEand before the TUI check, so an explicitly requested style always wins and the TUI correctly declines. Deciding it at life-cycle selection instead would have skippedNX_DEFAULT_OUTPUT_STYLEand landed after--output-stylewas already normalized.Decisions worth reviewing
The summary points at the raw log, not a stripped-ANSI sibling. That keeps NXC-4696 out of scope. It only works because nothing is inlined — an inlined excerpt would carry escape codes.
An earlier cut inlined the last ~20 lines of each failure. It was dropped. The argument for it is one fewer round trip; the arguments against won: every consumer of this style can read the file (it is chosen for agents or asked for explicitly), a fixed tail is as likely to catch a runner's footer as its error, and with 8 failures a 21-line-each bound is a transcript again.
The inline fallback for an unhashed task was removed as unreachable.
processTaskhashes before it schedules, and the onlytask.hash = undefinedin the codebase is a transient clear immediately followed by a re-hash — so a task with terminal output always has a hash. The remainingif (task.hash)is a type guard against printing a bogus path, and says so.Verification
tscclean onpackages/nxande2e/nx; 169 unit tests green;cargo checkpasses on the Rust change.e2e/nx/src/terminal-outputs.test.ts):cache:falseunderNX_BATCH_MODEand under--output-style=streameach leave a readable file; the cached replay path is unchanged; a--skip-nx-cacherecord is never served as a hit. Plus the summary cases — a passing run stays under 10 lines, a failing run under 30, an explicit style beats the agent default, and the printed path is opened and asserted to contain the task's output, which is the seam between the two tickets.cache.spec.ts(the committed.nodepredatesrecord_terminal_outputs). CI covers both.Related Issue(s)
Tracked in Linear rather than as GitHub issues:
--output-style=summary, auto-selected underisAiAgent()Both belong to the Terminal Logs available from disk milestone of Agent Friendly Task Output. NXC-4694 unblocks NXC-4696 and NXC-4695; NXC-4698 unblocks NXC-4699.
View Polygraph session ↗