Fix grounded follow-up audit findings#2
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new skip API to mark tasks as skipped (e.g., for cache hits) and updates the task event creation timestamp to use Date.now(). It also adds context tracking using AsyncLocalStorage to defer runtime closing when requested inside a scoped task callback. The review feedback highlights two critical issues: a potential memory leak where task handles are not disposed of if a scoped task callback throws an error, and a bug in #shouldDeferScopedClose where checking #handles.has(activeHandle) can prevent deferred closing if the handle was already manually disposed.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| } finally { | ||
| this.#activeScopedTasks -= 1; | ||
| await this.flush(); | ||
| if (this.#shouldRunDeferredScopedClose()) { | ||
| this.#closeRequestedByScopedTask = false; | ||
| await this.close(); | ||
| } | ||
| } |
There was a problem hiding this comment.
When a scoped task callback throws an error, the catch block handles updating the task status in the store, but the task handle itself is never disposed. This leads to a memory leak because the handle remains in the #handles set indefinitely, and any registered AbortSignal listeners are not cleaned up.\n\nTo fix this, we should explicitly call handle.dispose() in the finally block of runtime.task. Since dispose() is idempotent, this is safe to call even if the task succeeded and was already disposed.
} finally {\n handle.dispose();\n this.#activeScopedTasks -= 1;\n await this.flush();\n if (this.#shouldRunDeferredScopedClose()) {\n this.#closeRequestedByScopedTask = false;\n await this.close();\n }\n }| #shouldDeferScopedClose(): boolean { | ||
| const activeHandle = this.#taskCloseContext.getStore(); | ||
| return ( | ||
| activeHandle !== undefined && | ||
| this.#handles.has(activeHandle) && | ||
| this.#activeScopedTasks > 0 && | ||
| !this.#closing && | ||
| !this.#closed | ||
| ); | ||
| } |
There was a problem hiding this comment.
The check this.#handles.has(activeHandle) in #shouldDeferScopedClose can cause unexpected behavior. If a scoped task manually disposes its handle (e.g., by calling handle.succeed() or handle.fail()) and then calls runtime.close() from within its callback, the handle is no longer in #handles. This causes #shouldDeferScopedClose to return false, meaning the close is not deferred and the runtime is closed immediately, potentially cutting off other active parent or concurrent tasks.\n\nSince activeHandle is retrieved from this.#taskCloseContext (which is an instance-specific AsyncLocalStorage), any returned handle is guaranteed to belong to this runtime. We can safely remove the #handles.has check to ensure that close() is always deferred when called from within any scoped task callback, regardless of whether the handle has been manually disposed.
#shouldDeferScopedClose(): boolean {\n return (\n this.#taskCloseContext.getStore() !== undefined &&\n this.#activeScopedTasks > 0 &&\n !this.#closing &&\n !this.#closed\n );\n }
Summary
This PR applies only the follow-up review findings that were supported by current code, tests, docs, or direct reproduction.
Implemented fixes:
runtime.close()when it is called from inside the currently running scoped task callback, so the callback can resolve and the scoped task can become terminal before JSON summary finalization.updatedAtvalues when child progress changes.task.skip(message?)API becauseTaskStatus, summary counts, and rendering already supportedskipped, but callers had no handle method to produce it.bun run checkandbun run pack:checkon Node 24.contents: write.Tests added:
running: 0andsucceeded: 1.task.skip()emits skipped task status and increments skipped summary count.Local verification run in the analysis environment:
npx tsc -p tsconfig.json --noEmitnpx oxlint src testnpx oxfmt --check .rm -rf dist-test dist && npx tsc -p tsconfig.test.json && node --test "dist-test/test/**/*.test.js"npx tsc -p tsconfig.build.jsonnode test/fixtures/consumer-esm.mjsnpx tsc -p test/fixtures/consumer-ts/tsconfig.json --noEmitnpm pack --dry-run --jsonResult: 98 Node tests passed locally. The local runtime was Node 22, while this package targets Node 24+, so the new CI workflow should be treated as the final Node 24 gate.
Deliberately not included
stdoutoption removal/rename,attachLogStream, scaffold policy docs, and Node/npm fallback scripts: these are product/API/documentation tasks, not directly necessary to fix the reproduced failures in this PR.