Skip to content

feat: transactional notebook page discard with terminology realignment and spawn cleanup - #19

Open
ofriw wants to merge 37 commits into
mainfrom
notebook-deletion
Open

feat: transactional notebook page discard with terminology realignment and spawn cleanup#19
ofriw wants to merge 37 commits into
mainfrom
notebook-deletion

Conversation

@ofriw

@ofriw ofriw commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Note: This PR was generated by an AI agent. If you'd like to talk with other humans, drop by our Discord!


🧠 TL;DR

The notebook grew forever. Now it doesn't have to. This PR adds safe, transactional page discard — prune pages that hold only recoverable code facts, without data loss. Along the way the notebook's usage contract was redefined from a durable grounding store into a two-tier discardable cache ("grounding" → "memory"), and the agent's mental model was renamed from "job" → "topic" and "brief" → "prompt" because the old words encouraged the wrong behavior. Spawn cleanup error handling simplified and the spawn test suite hardened by removing a mock seam that let bugs pass.


✨ Headline: Safe notebook page discard

Layer What changed Detail
API prepareNotebookDiscard + commitNotebookDiscard Two‑phase protocol. Stage a discard, then commit. Partial failures roll back.
Handoff discardPages parameter Discard pages holding only recoverable code facts when compacting to a new topic; keep and refresh user guidance, decisions, design, and task scope. Only fires after successful compaction.
Rehydration Epoch‑marker gate A notebook-generation custom entry tracks the committed generation. Uncommitted entries from interrupted discards are ignored. No more ghost pages.
Branch scope session_tree reconstruction Notebook pages, committed epoch, and discard watermark follow the active session branch: /tree navigation switches the notebook view, branches diverge without cross-contamination, writes land on the current branch's generation.

📓 Notebook usage contract (what changed for the agent)

Beyond the discard plumbing, this PR changes how the agent is instructed to use the notebook:

  • Two-tier cache model — the notebook is now a cache for the work stream, not an archive: "stale pages mislead more than missing pages hurt". Recoverable code facts (APIs, structure, re-derivable findings) must not be hoarded and are discarded freely at handoff; non-recoverable knowledge (user guidance, decisions, design, task scope) is kept and refreshed, never left stale.
  • notebook_write content contract — capture only high-value knowledge for the current work stream (user guidance, decisions, design, constraints); keep re-derivable code facts minimal.
  • Shared memory for spawn — the notebook now doubles as shared memory between spawned agents and across handoff chains.
  • Pre-handoff ritual — before handoff the agent must update the notebook (discard recoverable code-fact pages, refresh non-recoverable knowledge), then list the pages and read the relevant ones to verify all important findings are persisted; chained handoffs use the notebook as storage and state management across contexts.
  • Post-handoff retention report — after a discard-using handoff the agent is told the outcome ("Handoff complete. Notebook: N pages kept, M discarded."), making retention observable.
  • Handoff tool contract — "AFTER HANDOFF the agent sees: the handoff prompt and the current notebook with optional pages discarded" (previously "all notebook pages").

♻️ Terminology realignment (every surface)

The old "one context, one job" model encouraged handoff at every workflow boundary (research → planning → execution) even when the subject was unchanged.

Before After Effect
job topic Agents stay put when working the same subject
brief prompt Clear directive ("do this"), not a summary ("here's what happened")
grounding memory Notebook redefined from durable grounding store → two-tier discardable cache (see "Notebook usage contract" above)

System prompt, tool descriptions, docs, and tests — all of them updated.


🔧 Spawn

Change Detail
Cleanup error handling spawnCleanupErrors WeakMap + getSpawnCleanupError() removed. Uses ctx.ui.notify() or chains to primaryError.cause. Smaller surface.
Terminology in child prompt "grounding knowledge" → "shared memory" (aligns with rename above)
Test suite hardened sessionFactory kept as a documented fault-injection seam; integration tests now exercise real createAgentSession end-to-end (real-session suite, no mocks).

🧪 Tests

  • Discard: prepare / commit / rollback / rehydration edge cases
  • Handoff discard: success, failure, empty (no‑op), parameter schema
  • Terminology: negative regex asserts old terms are absent
  • Spawn: mock-based unit tests retained for fault injection; real-session integration tests added

⚠️ Breaking changes?

None. discardPages is optional. Epoch‑marker rehydration falls back to legacy behavior. getSpawnCleanupError() removed but was internal-only. All renames are internal. Behavioral notes (not API-breaking): after handoff the agent now sees the current notebook with optional pages discarded (previously "all notebook pages"), and discard-using handoffs report retention counts.


The agent-readable change summary is inline above; a previous AGENT_REVIEW.md attachment link was removed to keep a single source of truth.

ofriw added 19 commits July 26, 2026 10:19
…ages

Epoch is now a predictable sequential integer (1) instead of Date.now(),
enabling the generation-based discard mechanism. Rehydration scans
generation markers to ignore staged survivors from interrupted discards.

DiscardPages lets agents prune stale notebook pages during handoff:
prepareNotebookDiscard stages survivors, commitNotebookDiscard advances
the epoch only after compaction succeeds. Interrupted discards leave
the branch on the prior generation.
…ions

Removes the sessionFactory parameter from executeSpawn and
registerSpawnTool, replacing mock-based lifecycle tests with pure
function tests and real child invocations via a deterministic provider.
Simplifies cleanup error handling from WeakMap to UI notify + cause chain.
Moves concurrent and abortion tests to real invocations.
@ofriw ofriw changed the title feat(handoff): transactional notebook discard during context handoff feat: transactional notebook page discard with terminology realignment and spawn cleanup Jul 29, 2026
@ofriw
ofriw requested a review from grzegorznowak July 29, 2026 13:38

@grzegorznowak grzegorznowak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @ofriw here's a review in an alternative form I've been experimenting with recently.
it's not really grilled much beyond reformatting it to this shape, so some points might be just a noise. Leaving with you as-is, hopefully you can run it by your agent for some quick pushback 🙏🏾


Business and user-facing issues

1. Switching conversation branches can leave the wrong notebook loaded

Imagine:

  1. Branch A has notebook pages about authentication.
  2. You switch to Branch B, which is about payments.
  3. The extension switches the conversation, but its in-memory notebook still contains Branch A’s pages.
  4. You discard a page or write a new one.
  5. Those operations can affect Branch B using Branch A’s notebook state.

This is dangerous because information from one conversation path can leak into or overwrite another path.

The fix is to reload the notebook whenever the user switches branches, not only when a session starts.

2. A successful deletion can be reported as unsuccessful

The discard process does two things:

  1. Persist the deletion.
  2. Show a notification and send the next message.

Those operations are currently inside one error-handling block.

For example:

  1. The extension successfully deletes old-research.
  2. The UI notification unexpectedly fails.
  3. The error handler assumes the deletion failed.
  4. It says, “All notebook pages were retained.”
  5. But old-research is already gone.

The main problem is misleading recovery information. A user may believe the page still exists when it does not.

The fix is to handle persistence errors separately from notification errors.

3. One instruction still encourages unnecessary handoffs

The new intended rule is:

Stay in the same context while the topic remains the same.

But the notebook guidance also says:

The immediate next task belongs in handoff.

Consider one topic: “Add OAuth login.”

  • Phase 1: research the current authentication code.
  • Phase 2: plan the change.
  • Phase 3: implement it.
  • Phase 4: test it.

These are different phases, but they are all the same topic. The remaining instruction may cause the agent to hand off between every phase, which is exactly the
behavior this PR is trying to eliminate.

The wording should say that the next task belongs in a handoff only when the topic changes or the context becomes too noisy.

4. It is unclear whether notebook pages should follow a new topic

The documentation effectively promises:

Memory from an unrelated topic will not automatically follow you.

But the implementation does this:

  • Every page that is not explicitly discarded remains available.
  • The next context receives every retained page’s name and first-line preview.
  • The agent is also encouraged to permanently delete pages that are not relevant to the immediate next context.

Example:

  1. Topic A contains confidential customer-debugging notes.
  2. You hand off to unrelated Topic B.
  3. If the pages are retained, their names and previews appear in Topic B.
  4. If they are discarded, they are permanently unavailable when you return to Topic A.

The product needs one clear rule. For example, pages could belong to explicit topics and only pages for the active topic would be shown. “Not currently relevant”
should also not automatically mean “safe to delete forever.”

5. The notebook index cannot prove that the notebook is complete

Before handoff, the agent is instructed to scan the notebook index and verify that all important findings were saved.

But the index only displays something like:

oauth-decisions: Use PKCE for the authorization flow...

It does not display the full page.

Suppose the full page should contain:

Use PKCE.
Refresh tokens expire after 30 days.
Admin accounts require an additional scope.
The old callback route must remain supported.

If only the first line was saved, the index still looks correct. The agent could proceed with the handoff and lose the other details from the conversation.

The instruction should require opening and checking relevant pages, not merely confirming that their names exist.

Technical issues

6. Saved notebook records are trusted without enough validation

Notebook data comes from persisted session history. The code assumes those records have the expected structure.

A valid record might look like:

{
"version": 1,
"epoch": 3,
"name": "oauth-notes",
"content": "Use PKCE"
}

But the code does not properly reject records such as:

{
"version": 99,
"epoch": 500
}

That future or malformed generation marker could make the extension conclude that epoch 500 is current. All legitimate pages from epoch 3 would then appear outdated
and disappear from the in-memory notebook.

It also accepts any truthy value as a page name, including a number or object, even though notebook tools require string names.

The fix is runtime validation:

  • Recognize only supported versions.
  • Require plain objects.
  • Require non-empty string page names.
  • Require string content.
  • Treat legacy records through an explicit compatibility path.
  • Safely ignore unknown future records.

7. A failed UI notification can hide the real spawn failure

Suppose three failures happen:

  1. The child agent fails: API request failed.
  2. Cleaning up its session fails: dispose failed.
  3. Showing the cleanup notification fails: UI closed.

The caller may receive only:

UI closed

That hides the useful error, API request failed, as well as the cleanup problem.

The headless path already combines multiple failures into an AggregateError. The UI path should similarly protect the original error even if notification fails.

8. npm_execpath is not validated as a real executable script

npm_execpath is an environment value telling the compatibility scripts where npm’s JavaScript entry point is.

The validator currently checks roughly:

  • Does something exist at this path?
  • Is its name npm or npm-cli?

That “something” could be a directory rather than a file.

Relative paths are also dangerous. For example:

npm_execpath=tools/npm-cli.js

The validator checks that path relative to the current process directory. Later, the subprocess may run from a different directory, so the same relative path points
somewhere else and fails.

The fix is to require an absolute path to a supported regular file, or resolve it to an absolute path before changing directories.

9. Spawn tests still bypass the real production session creation

The PR says it hardened spawn testing by removing a mock session factory. The current production functions still accept that factory, and many tests still provide
simplified fake sessions.

A fake session might implement only:

{
prompt() {},
abort() {}
}

The real SDK session may also have important initialization, disposal, event, and ownership behavior. A bug in that real setup can therefore be missed while the
fake-session test passes.

Mocks are still useful for forcing unusual failures. The problem is the mismatch between the stated goal and the coverage. The project should either:

  • remove the factory override and test through the real SDK, or
  • document it as an intentional fault-injection mechanism and maintain a separate real lifecycle test matrix.

10. The import-order tests do not reproduce the complete failure sequence

The original regression apparently required this order:

Load handoff
→ load spawn
→ create a real agent session

The tests prove two different halves:

Test A:
Load SDK
→ load handoff
→ create session

Test B:
Load handoff
→ load spawn
→ check that SDK functions exist

Neither test performs the exact failing sequence. A bug could leave createAgentSession present as a function but cause it to fail when called after handoff-first
initialization.

A fresh-process test should load handoff first and then construct a real session in that same process.

11. The security exception may hide a future vulnerable dependency

The project temporarily allows one security advisory because a known dependency path still contains the vulnerable package.

The exception is broad: it permits the advisory everywhere.

The compensating test checks that the currently installed dependency tree contains only the expected vulnerable path. However, an npm lockfile can include
dependencies for other operating systems or CPU architectures that are not installed on the current machine.

Example:

Expected path:
pi-coding-agent → minimatch → vulnerable brace-expansion

New unnoticed path:
some-arm-only-package → vulnerable brace-expansion

On an x64 CI runner, the ARM-only package may not appear in npm ls. The test still passes, while the broad advisory exception suppresses the new vulnerability.

The invariant should inspect the complete lockfile rather than only the packages installed on the current runner.

@ofriw

ofriw commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

PR #19 Review Response - Pushback on Inaccuracies

Thank you for the thorough review! Most of your points are valid and fixes were implemented. However, a couple of points contain factual inaccuracies that I want to clarify:


Point 3: "One instruction still encourages unnecessary handoffs"

This is based on a misreading of the instruction.

The phrase "the immediate next task belongs in handoff" appears in notebook/tools.ts line 79 as a notebook write guideline, not a handoff trigger rule. The full context:

"Avoid transient task state, scratch reasoning, transcripts, logs, or large tool output; the immediate next task belongs in handoff."

This means: "When writing to the notebook, don't put the next task there—put it in the handoff prompt instead." It's about what goes where (notebook vs. handoff prompt), not when to trigger handoff.

The handoff trigger rules are unambiguous throughout the codebase:

System prompt:

  • "When the topic changes, or when context is noisy past the ~30% heuristic, use handoff."
  • "same topic → spawn bias, different topic → handoff bias"
  • "If yes [work fits current topic], break it into phases... and delegate via spawn. If it doesn't fit the current topic, prefer handoff."

Handoff tool description:

  • "WHEN TO USE: ... 3. The current topic is complete and a new distinct task starts."

For your OAuth example (research → plan → implement → test), all phases are the same topic, so the system prompt says "same topic → spawn bias" and "delegate >10k-token sub-tasks via spawn." No handoff between phases unless the topic actually changes.

No code change needed. The instructions are consistent—there's no contradiction.


Point 9: "Spawn tests still bypass the real production session creation"

The implication that tests bypass real sessions is misleading.

You're correct that the PR description overstates by saying "Removed mock sessionFactory seam" when it still exists. I'll fix that description. However, your broader claim that tests "bypass the real production session creation" misses that real integration tests exist:

  1. Real integration tests exist: spawn-runtime-compatibility.test.ts uses createAgentSession (no mocks) and tests SDK initialization, provider execution, tools, thinking clamping, abort/reset, and output behavior. The file explicitly documents: "Uses real createAgentSession (no mocks)."

  2. Unit tests use fakes intentionally: spawn.test.ts injects custom factories to test specific error handling paths (e.g., "what if session.dispose() throws?", "what if cleanup fails after primary error?"). This is standard unit testing practice—testing the consumer in isolation to force specific failure modes that are difficult to trigger with real sessions.

  3. Factory is a test seam by design: sessionFactory: typeof createAgentSession = createAgentSession defaults to the real implementation in production. The parameter exists to enable fault injection in tests, not to bypass production code.

Your concern about "important initialization, disposal, event, and ownership behavior" being missed is valid but overstated. The real-session tests cover SDK initialization and disposal. The unit tests with fakes cover error paths that are difficult to force with real sessions.

The gap is in the PR description accuracy, not in test coverage. I'll fix the description to say "Reduced reliance on mock sessionFactory seam. Integration tests now use real createAgentSession; unit tests retain factory for fault injection."


All other points are valid and I'm implementing fixes. Thanks again for the detailed review!

@ofriw
ofriw requested a review from grzegorznowak July 30, 2026 12:45

@grzegorznowak grzegorznowak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fixes so far. These are the five remaining issues I think matter most before merge:

  1. Discard retries can resurrect pages. Every prepare attempt stages survivors at state.epoch + 1. After a failed attempt, a retry reuses that epoch, so orphaned survivors from the failed attempt become visible when the retry commits. Example: fail while discarding B, then successfully discard C; rehydration can restore A, B, and C. (notebook/store.ts:128-136, notebook/rehydration.ts:75-83)

  2. Branch switching leaves the previous branch's notebook loaded. Notebook rehydration runs only on session_start; session_tree invalidates handoff/readonly state without rebuilding notebookPages or epoch. Reads, writes, or discards after navigation can therefore use another branch's memory. (notebook/rehydration.ts:43-48, index.ts:687-693)

  3. A committed deletion can be reported as unsuccessful. commitDiscard, status clearing, notification, and sendUserMessage share one try. If UI or messaging fails after the commit, the catch path says the discard was not persisted and all pages were retained even though deletion already happened. (handoff/tool.ts:137-156, notebook/store.ts:146-151)

  4. Handoff wording can put temporary state into durable memory. The main prompt correctly separates durable notebook knowledge from situational handoff direction, but the tool says to promote "any missing knowledge" into the notebook. A literal model can include temporary blockers and immediate next steps. Please say "missing durable, reusable knowledge" and keep current state/blockers/next steps explicitly in the handoff prompt. (handoff/tool.ts:185-200, system-prompt.ts:53-66)

  5. The exact handoff-first production sequence is still not tested successfully. One test constructs a valid real session but imports the SDK first; the fresh-process test loads handoff first but uses intentionally invalid session arguments. Please add one fresh-process test that loads handoff, loads spawn, and successfully constructs a valid deterministic-provider session. (tests/unit/spawn-after-handoff.test.ts:23-55, tests/unit/module-evaluation-order.test.ts:37-56)

Detailed reasoning, examples, and requested changes: DETAILS.md

@ofriw
ofriw requested a review from grzegorznowak August 4, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants