fix(coding-agent): ensured top-level declarations persist in async cells

- Updated import rewriting to identify and publish `var` and `function` declarations to the global scope when a cell contains top-level `await`.
- Prevented these declarations from being trapped within the async wrapper's function scope, allowing them to remain accessible to subsequent evaluation cells.
This commit is contained in:
can1357
2026-07-12 13:21:45 +02:00
parent f933f02fcf
commit b6f83021c9
3 changed files with 72 additions and 13 deletions
+5
View File
@@ -2,6 +2,11 @@
## [Unreleased]
### Fixed
- Improved search reliability for Perplexity provider by forcing retrieval for all queries
- Fixed JS eval cells losing top-level `function` and `var` declarations across cells when the defining cell contained top-level `await` — the async wrapper scoped them to the cell's IIFE instead of publishing them to the worker global
## [16.4.7] - 2026-07-12
### Added
@@ -48,6 +48,15 @@ type BabelClassDeclaration = {
};
type BabelLexicalDecl = BabelVariableDeclaration | BabelClassDeclaration;
type BabelFunctionDeclaration = {
type: "FunctionDeclaration";
start: number;
end: number;
id: { start: number; end: number; name: string } | null;
};
/** Top-level declarations whose bindings must survive the cell (demoted and/or published). */
type BabelPublishableDecl = BabelLexicalDecl | BabelFunctionDeclaration;
type BabelExpressionStatement = {
type: "ExpressionStatement";
@@ -322,7 +331,7 @@ function collectBindingNames(pattern: unknown, names: string[]): void {
}
}
function getLexicalBindingNames(node: BabelLexicalDecl): string[] {
function getLexicalBindingNames(node: BabelPublishableDecl): string[] {
const names: string[] = [];
if (node.type === "VariableDeclaration") {
for (const declaration of node.declarations ?? []) collectBindingNames(declaration.id, names);
@@ -348,40 +357,49 @@ function appendGlobalBindingPublish(source: string, names: readonly string[]): s
* let { a, b } = obj; -> var { a, b } = obj;
* class Foo extends Bar {} -> var Foo = class extends Bar {};
*
* When the source must run inside the async wrapper, demoted `var`s would normally become
* function-scoped. In that mode we publish each top-level binding back to the wrapper's
* lexical `this`, which is the worker global object.
* When the source must run inside the async wrapper (top-level `await`), demoted `var`s —
* and the user's own top-level `var` and `function` declarations — would be scoped to the
* wrapper function and die with the cell. In that mode we publish every top-level binding
* back to the wrapper's lexical `this`, which is the worker global object.
*
* Nested declarations (inside functions, blocks, classes) are left alone \u2014 they're
* Nested declarations (inside functions, blocks, classes) are left alone — they're
* scoped to their enclosing function/block regardless of `var` vs `let`/`const`.
*/
async function demoteTopLevelLexicals(code: string, options: { publishGlobals?: boolean } = {}): Promise<string> {
if (!/\b(?:const|let|class)\b/.test(code)) return code;
const publishGlobals = options.publishGlobals === true;
const fastPath = publishGlobals ? /\b(?:const|let|class|var|function)\b/ : /\b(?:const|let|class)\b/;
if (!fastPath.test(code)) return code;
const ast = await parseProgram(code);
if (!ast) {
return code;
}
const targets: BabelLexicalDecl[] = [];
const targets: Array<{ node: BabelPublishableDecl; demote: boolean }> = [];
for (const node of ast.program.body) {
if (node.type === "VariableDeclaration") {
const decl = node as unknown as BabelVariableDeclaration;
if (decl.kind === "const" || decl.kind === "let") targets.push(decl);
if (decl.kind === "const" || decl.kind === "let") targets.push({ node: decl, demote: true });
else if (publishGlobals) targets.push({ node: decl, demote: false });
} else if (node.type === "ClassDeclaration") {
const decl = node as unknown as BabelClassDeclaration;
if (decl.id) targets.push(decl);
if (decl.id) targets.push({ node: decl, demote: true });
} else if (publishGlobals && node.type === "FunctionDeclaration") {
const decl = node as unknown as BabelFunctionDeclaration;
if (decl.id) targets.push({ node: decl, demote: false });
}
}
if (targets.length === 0) return code;
targets.sort((a, b) => b.start - a.start);
targets.sort((a, b) => b.node.start - a.node.start);
let result = code;
for (const node of targets) {
for (const { node, demote } of targets) {
const segment = result.slice(node.start, node.end);
const bindingNames = options.publishGlobals ? getLexicalBindingNames(node) : [];
const bindingNames = publishGlobals ? getLexicalBindingNames(node) : [];
let replacement: string;
if (node.type === "VariableDeclaration") {
if (!demote) {
replacement = segment;
} else if (node.type === "VariableDeclaration") {
replacement = `var${segment.slice(node.kind.length)}`;
} else {
const id = node.id;
@@ -182,3 +182,39 @@ describe("rewriteImports", () => {
expect(wrapped.source).not.toContain(`${IMPORT} type`);
});
});
// Cross-cell persistence: cells run through indirect eval, so bindings survive only if
// they land on globalThis. Cells with top-level `await` run inside an async IIFE, where
// declarations are function-scoped — wrapCode must publish them back to the global object
// or the next cell sees ReferenceError.
describe("wrapCode cross-cell persistence", () => {
it("publishes top-level function declarations and their sibling consts from async-wrapped cells", async () => {
const globals = globalThis as Record<string, unknown>;
const wrapped = await wrapCode(
"async function ompPersistedFn(n) { return (await Promise.resolve(n)) + 1; }\nconst ompPersistedTotal = await ompPersistedFn(41);",
);
expect(wrapped.asyncWrapped).toBe(true);
try {
await indirectEval(wrapped.source);
const fn = globals.ompPersistedFn as (n: number) => Promise<number>;
expect(typeof fn).toBe("function");
expect(await fn(1)).toBe(2);
expect(globals.ompPersistedTotal).toBe(42);
} finally {
delete globals.ompPersistedFn;
delete globals.ompPersistedTotal;
}
});
it("publishes explicit top-level var declarations from async-wrapped cells", async () => {
const globals = globalThis as Record<string, unknown>;
const wrapped = await wrapCode("await Promise.resolve();\nvar ompPersistedVar = 5;");
expect(wrapped.asyncWrapped).toBe(true);
try {
await indirectEval(wrapped.source);
expect(globals.ompPersistedVar).toBe(5);
} finally {
delete globals.ompPersistedVar;
}
});
});