With the new CookieStore, the session must be freed before the notification is.
This is how it works in CDP, but in fetch, we were pretty lazy about it. This
caused the notification to be freed first, and then the cookiestore to try to
unregister: UAF.
* tools: add session-scoped bridge store
Exposes `globalThis.lp` to `/eval` calls, allowing state to persist
across evaluations and page navigations. Adds a `save` parameter to
both `/eval` and `/extract` to store results in the bridge.
* browser: await promises in eval and support inline args
- Await JS Promises in `eval` tool with a 30s timeout
- Support inline arguments in multi-line slash commands
- Silence output on successful `save=`
- Add `limit` option to extract schema walker
* eval: return empty text for undefined async IIFE
* extract: support limit on simple string arrays
Treats `["<sel>"]` as sugar for `[{"selector": "<sel>"}]` in the schema
walker. This enables the `"limit"` option on simple string arrays.
Also updates agent documentation to cover cross-call state with `lp.*`.
* refactor: optimize bridge store and schema lookup
- Introduce `bridgeStorePut` to skip redundant JSON validation for
trusted stringified values in `bridgeSync`.
- Store the schema pointer in `BlockOpener` to avoid re-parsing and
looking up the schema in `Iterator.next`.
- Clean up error handling and optional unwrapping in `execEval`.
- Remove unused parameters from save helper functions.
- Inline and simplify save path duplication and file writing.
- Clean up optional unwrapping for the recorder.
Add a REPL-only `/save [filename.lp]` command that persists the current
interactive session as PandaScript without requiring the user to start the
agent with `-i <script>`. The command records the same replayable actions as
the existing script recorder, but keeps them in memory until the user chooses
to save.
Functional behavior:
- During a REPL session, record replayable browser actions into an in-memory
script buffer.
- Manual slash commands are recorded through the same PandaScript formatting
and filtering rules used by file recording.
- Natural-language turns record their prompt as a `# ...` comment only when
the LLM turn produces at least one successful replayable tool call.
- Failed LLM tool calls are skipped, and repeated successful `/extract` calls
keep only the last successful extract, matching the existing recorder logic.
- `/save filename.lp` writes the current in-memory recording to that file.
- Bare `/save` creates a random `session-<hex>.lp` file on first save.
- If the first save targets an existing file, prompt with the existing numbered
TTY picker and ask whether to replace or append.
- After the first successful save, the REPL session is locked to that filename:
later `/save` or `/save same-file.lp` appends to the same file without
prompting, while `/save other-file.lp` is rejected.
- After each successful save, reset the in-memory recorder so future saves only
append actions entered since the previous save.
- On any `/save` error or cancellation, keep the in-memory recorder intact so
the user can retry without losing captured actions.
- Restrict `/save` filenames to local file names, not paths, to keep behavior
scoped to the current directory.
Code changes:
- Add `Recorder.Memory`, an in-memory recorder that shares the existing
`Command.isRecorded`, `Command.format`, comment formatting, and `LP_*`
reverse-substitution behavior with file recording.
- Add `Recorder.Memory.reset()` so `/save` can clear only successfully saved
deltas.
- Add `/save` to the REPL meta command table and help/completion surface.
- Add `save_buffer` and `save_path` state to `Agent`.
- Feed manual REPL tool calls into `save_buffer` alongside the existing optional
file recorder.
- Extend LLM turn recording with a `capture_for_save` flag so natural-language
REPL turns can be captured without affecting non-REPL script/self-heal paths.
- Implement `/save` handling in `Agent`:
- parse and validate the optional filename,
- choose replace/append for existing first-save targets,
- remember the first successful save path,
- enforce the single-destination rule for the rest of the session,
- append later deltas by default,
- commit remembered path state only after a successful file write.
- Add focused coverage for the memory recorder’s filtering and reset behavior.
This is a bit all over the place.
1 - Replace libidn2 with rust-idna. It looks like there are different idna
profiles, and rust-idna (from the servo project) implements the whatwg
one. libidn2 would be too strict in some cases and not strict enough in
others. (Gemini says I could use libidn2 for this, but what it suggested
didn't work, and I couldn't figure it out myself, and claude insisted it
_did not_ have the correct implementation for what we want).
2 - We previously only ran a URL through idna if it wasn't ascii. Turns out
we also need to run it if there's a "xn--" (aka, an IDNA ACE prefix) in
there. This helps us pass hundreds of WPT cases, and it's pretty cheap.
3 - Implement more of the Area WebAPI. Mostly copied from Anchor.
4 - Add username/password accessor to Anchor/Area
5 - window.open validates the URL (i.e. tries to resolve it and handles the
error)
6 - Invalid idna conversion maps to a TypeError
7 - Cleanup closed popups on the next tick (like destroyed pages), rather than
at an interval or on shutdown. This one seems unrelated, but some of these
tests are opening hundreds (thousands?) of popups and then closing them.
If a synthetic url (blob URL) causes a navigation event, the frame abort will
deinit the transfer, causing the `defer transfer.deinit()` atop Synthetic.run
from firing. Flag the transfer as .completing to prevent this from happening.
This mimics what non-synthetic urls do.
Adds a pretty simplistic Notification WebAPI. Also adds a dummy drawImage to
CanvasRenderingContext2D.
Trying to improve how we're seen by https://bot.sannysoft.com/
Previously, Cookie.Jar.add would only conditionally take over the cookie. The
caller had no way to know whether or not to deinit it. This could result in
a double-free on certain error paths.
Cookie.Jar.add now unconditionally takes ownership of the cookie.
Move the per-Window storage bucket to an origin-keyed Shed on the Session
so localStorage and sessionStorage survive navigation within an origin,
matching the Web Storage spec.
Also fixes two pre-existing bugs surfaced by this work:
- setItem's quota counter was incremented on every call, never
decremented on overwrite — five same-key overwrites tripped the cap
spuriously. Now subtracts the old value's length first.
- Shed.getOrPut used allocator.free on a single-pointer allocation
where allocator.destroy was required, and inserted into _origins
before its dependent allocations could fail. Reordered so the entry
is only put once both key dupe and bucket creation have succeeded.
Adds an MCP test that round-trips localStorage between two origins via
the eval tool to lock in the persistence + isolation contract.
I noticed that `fetch www.openmymind.net` worked but, `fetch www.example.com`
didn't. www.openmymind.net redirects to `https://www.openmymind.net/` so
in `frameHeaderDoneCallback` we get the updated response.url(). www.example.com
doesn't redirect, so self.url remains `www.example.com` which just doesn't work
at various parts of the code (Location.init, RobotsLayer...).
Added a quick check in Navigate, if the URL isn't a "complete" URL, stick
"http://" infront. There are probably cases where this is wrong, e.g.
'javascript:...' but these don't work anyways.
(Curl works with www.example.com of course).
I'm not sure how this is happening, but we occasionally see an attempt to
execute a null v8::Function (from crash reports). It seems pretty clear that
this is a Global that is being reset, but I can't figure out a path where
a global is reset while a callback is still active. So, in != .Debug mode, we
null check functions before executing and return an error.
This potentially will work without causing any new/different issues. If this is
a global being reset, than we must be in some teardown state, and it probably
doesn't matter if a JS callback is or isn't executed.
In case anyone ends up here and wonders about the guard, here's a stack from
a crash dump:
```
Fatal V8 Error
---
location: v8::Function::Call
message: Function to be called is a null pointer
???:?:?: 0x2ac0b59 in ??? (???)
???:?:?: 0x2ace19b in ??? (???)
/src/browser/js/Function.zig:160:41: 0x268db23 in callWithThis__anon_236231 (lightpanda)
/src/browser/EventManagerBase.zig:259:30: 0x2614b53 in dispatchDirect__anon_179346 (lightpanda)
/src/browser/EventManager.zig:137:33: 0x261737e in stateChanged (lightpanda)
/src/browser/webapi/net/XMLHttpRequest.zig:580:30: 0x2621b21 in handleError (lightpanda)
/src/browser/webapi/net/XMLHttpRequest.zig:542:21: 0x2628bbd in httpErrorCallback (lightpanda)
/src/network/layer/Forward.zig:70:13: 0x29193de in errorCallback (lightpanda)
/src/browser/HttpClient.zig:1447:36: 0x2784aa6 in processMessage (lightpanda)
/src/cdp/CDP.zig:314:24: 0x26a3244 in dispatchParsed (lightpanda)
/src/cdp/CDP.zig:187:31: 0x252df34 in drainInbox (lightpanda)
/src/browser/HttpClient.zig:416:24: 0x27f01ab in handleConnection (lightpanda)
/home/runner/work/_temp/91356985-8cc1-40de-b1b8-395a29b4bd91/zig-x86_64-linux-0.15.2/lib/std/Thread.zig:509:13: 0x26877f3 in entryFn (lightpanda)
???:?:?: 0x7df4279d11f4 in ??? (libc.so.6)
Unwind information for `libc.so.6:0x7df4279d11f4` was not available, trace may be incomplete
```