diff --git a/docs.json b/docs.json
index 21af0a87..cfeaa2be 100644
--- a/docs.json
+++ b/docs.json
@@ -353,6 +353,7 @@
"sdk/guides/plugins",
"sdk/guides/convo-persistence",
"sdk/guides/context-condenser",
+ "sdk/guides/convo-notes-and-history",
"sdk/guides/persistent-memory",
"sdk/guides/agent-settings",
"sdk/guides/parallel-tool-execution",
@@ -881,4 +882,4 @@
"destination": "/openhands/usage/automations/overview"
}
]
-}
\ No newline at end of file
+}
diff --git a/sdk/guides/context-condenser.mdx b/sdk/guides/context-condenser.mdx
index 5ffadcef..d19f5b98 100644
--- a/sdk/guides/context-condenser.mdx
+++ b/sdk/guides/context-condenser.mdx
@@ -19,6 +19,9 @@ The context condenser solves this by intelligently summarizing older parts of th
## Default Implementation: `LLMSummarizingCondenser`
+For an opt-in mode that saves agent-written notes and retrieves original events
+after a context reset, see [Notes, History Retrieval, And Storage Protection](/sdk/guides/convo-notes-and-history).
+
OpenHands SDK provides `LLMSummarizingCondenser` as the default condenser implementation. This condenser uses an LLM to generate summaries of conversation history when it exceeds the configured size limit.
### How It Works
diff --git a/sdk/guides/convo-notes-and-history.mdx b/sdk/guides/convo-notes-and-history.mdx
new file mode 100644
index 00000000..de9fa28d
--- /dev/null
+++ b/sdk/guides/convo-notes-and-history.mdx
@@ -0,0 +1,259 @@
+---
+title: "Notes, History Retrieval, And Storage Protection"
+description: "Keep editable notes across context windows, retrieve original events, and pause before local disks fill."
+---
+
+`NotesRetrievalCondenser` is an opt-in alternative to the default summarizing
+condenser. It reminds the agent to save notes before a context reset, then replaces
+older context with an index pointing to notes and retrievable history. The original
+events remain in the conversation's event log.
+
+## Enable Notes And Retrieval
+
+Use an [already configured LLM](/sdk/getting-started) with the optional native tools:
+
+```python
+from openhands.sdk import (
+ Agent,
+ Conversation,
+ NotesRetrievalCondenser,
+ StorageSafetyConfig,
+)
+
+# Use your configured LLM and add your task tool specs to tools.
+agent = Agent(
+ llm=llm,
+ tools=[],
+ include_default_tools=[
+ "FinishTool",
+ "ThinkTool",
+ "ContextNotesTool",
+ "ConversationHistoryTool",
+ "NewContextTool",
+ ],
+ condenser=NotesRetrievalCondenser(max_size=240),
+)
+conversation = Conversation(
+ agent=agent,
+ workspace=".",
+ persistence_dir="./.conversations",
+ storage_safety=StorageSafetyConfig(min_free_ratio=0.05),
+)
+```
+
+The notes and history tools are required by this condenser. `NewContextTool` is
+optional: it lets the agent request a reset after a complete batch of tool results
+has been recorded. Threshold-based resets still apply without that tool. These are
+native SDK tools; no MCP server is required.
+
+The condenser can send one reminder per context window before its hard threshold.
+`max_size` limits the number of events in the active view. For token pressure, the
+effective limit is the smaller of `max_tokens` (when configured) and the model's
+known input limit. `reminder_fraction` defaults to `0.8` of either threshold.
+An input that jumps directly past a hard threshold may reset without a reminder.
+The condenser keeps
+the configured initial and recent events, system prompts, the latest user message,
+and complete tool exchanges. If those protected events alone prevent a smaller
+context that fits the budget, it raises an error instead of repeatedly resetting.
+
+Use a persistent local directory for restart durability. An in-memory conversation
+does not provide durable storage. The default condenser and existing conversations
+remain unchanged unless the new features are selected explicitly.
+
+## Enable Through Settings
+
+For settings-based launches, select `NotesRetrievalCondenserSettings`. The settings
+factory adds `ContextNotesTool` and `ConversationHistoryTool` when this condenser
+is enabled, including when `tools` is omitted or empty. Existing explicit specs
+for either tool are retained without duplication. Other configured tools and the
+stored settings are unchanged.
+
+```python
+from openhands.sdk import OpenHandsAgentSettings
+from openhands.sdk.settings import NotesRetrievalCondenserSettings
+
+settings = OpenHandsAgentSettings(
+ llm=llm,
+ condenser=NotesRetrievalCondenserSettings(enabled=True, max_size=240),
+)
+agent = settings.create_agent()
+```
+
+As with other settings-based agents, the runtime must register the default task
+tools before starting a conversation. The Agent Server handles that registration;
+standalone SDK applications can call
+`openhands.tools.preset.default.register_default_tools(enable_browser=False)`.
+Use `tools=[]` if no task tools are needed; the two required memory tools are still
+added while this condenser is enabled.
+
+`NewContextTool` remains optional and must be selected explicitly for manual
+resets. Disabled notes settings do not add either memory tool. Direct `Agent(...)`
+construction still requires explicit memory tools, as shown above.
+
+## Write Long Notes Without Truncation
+
+The agent calls `context_notes` with a `command`:
+
+| Command | Behavior |
+| --- | --- |
+| `write` | Replace the current notes with `content`. |
+| `append` | Append exactly the supplied `content`, including any desired newline. |
+| `read` | Return a page with text, `version_id`, `total_chars`, and `next_offset`. |
+
+Each write or append accepts up to 16,000 characters. An oversized mutation fails
+as a whole and leaves the current notes unchanged. Longer notes can be saved with
+a write followed by multiple appends; there is no 16,000-character aggregate cap.
+Append events store only the new piece, not another copy of the cumulative notes.
+
+Reads default to 4,000 characters and allow up to 8,000. Reuse the returned
+`version_id` with subsequent offsets to read a consistent version. A version is
+the ID of a successfully committed notes observation. Normal secret masking applies.
+Reads, failed writes, and uncommitted actions do not create notes versions.
+
+A mutation becomes durable only when the agent loop commits its successful
+observation to persistent storage. Calling `conversation.execute_tool()` directly
+with `write` or `append` returns an observation without committing it, so it does
+not save notes. Direct `read` calls can inspect already committed notes.
+
+Mutations in a tool batch commit in call order. A read in that same batch sees the
+previously committed notes; read again in the next turn to observe the mutations.
+Versions are scoped to the active conversation branch.
+
+## Retrieve Earlier Events
+
+`conversation_history` searches literal text in the current conversation's active
+branch and reads events by ID, including events removed from the model's context.
+Search previews and reads are bounded and paginated; the source event is not
+truncated. Provider reasoning and internal state are not exposed by this tool.
+The retrieval tools' own traffic is excluded from history search to avoid echoes.
+
+Store useful event IDs in notes rather than copying large tool outputs. Event
+files remain the source of truth for recovery and branching: do not delete
+individual JSON files to free space. Condensation changes the model's view, not
+the disk contents.
+
+## Pause Below Five Percent Free Space
+
+`StorageSafetyConfig` is independent of condensation and disabled by default.
+When enabled, it checks the filesystem holding local persistence and the local
+workspace. The ratio uses available bytes for the current process divided by total
+filesystem capacity. `min_free_ratio` defaults to `0.05` and can be configured.
+Below the selected threshold, new work is refused. Known writes that would cross
+the threshold are also refused, allowing for the complete temporary file needed
+by atomic replacement.
+
+Active runs check every five seconds and at execution boundaries. Already-started
+results and stop metadata may use the remaining space. The watchdog does not kill
+in-flight asynchronous tool operations. After freeing space or expanding storage,
+explicitly call `run()` or `arun()` again; the runtime rechecks capacity before
+resuming. It does not automatically delete notes, old versions, or events.
+
+Catch `StorageSafetyError` for structured diagnostics (`code`, free and total
+bytes, threshold, pending write size, and shortage). An optional
+`storage_safety_callback` receives errors independently of event persistence.
+
+This is an early-stop mechanism, not an operating-system disk quota. Other
+processes or an already-running command can still exhaust the remaining space.
+Committed records are retained. Outputs not yet persisted when the disk fails
+cannot be guaranteed recoverable after process exit.
+
+Checks apply at SDK-controlled admission and persistence boundaries. For a custom
+`AgentBase` implementation or an ACP agent, calls and file writes inside the custom
+step or external process may be opaque to the SDK. The SDK cannot admit or stop
+each of those internal operations separately. Use operating-system or container
+limits when you need an enforced disk quota.
+
+## Server Configuration And Recovery
+
+Set deployment configuration for all server conversations:
+
+```bash
+export OH_STORAGE_SAFETY_MIN_FREE_RATIO=0.05
+```
+
+Remote conversations use the server's disk checks, not the client's free space.
+Conversation request settings do not disable the deployment policy, and delegated
+tasks inherit it.
+Low-space HTTP requests return status `507` with structured diagnostics. Failures
+during background runs also reach connected clients without requiring a new event
+to be written successfully.
+
+A normal capacity pause only needs space and an explicit retry. An actual write
+failure requires inspection: a tool may have changed files even though its result
+was not recorded. Inspect those effects, then reconcile without rerunning the tool:
+
+
+
+
+```python
+conversation.recover_storage(acknowledge_unknown_outcomes=True)
+# Write-failure recovery leaves the conversation paused. Resume separately:
+conversation.run()
+```
+
+
+
+
+For an existing `RemoteConversation` connected to a supporting Agent Server:
+
+```typescript
+await conversation.recoverStorage({
+ acknowledge_unknown_outcomes: true,
+ // For ambiguous crash history only, choose an inspected event:
+ // head_event_id: "inspected-event-id",
+});
+// Recovery refreshes remote state but does not start a run:
+await conversation.run();
+```
+
+The client and server must both include the recovery API. The lower-level
+`ConversationClient.recoverStorage(conversationId, request)` returns the server's
+success response. HTTP errors preserve structured diagnostics in
+`HttpError.response`.
+
+
+
+
+For Agent Server, use `POST /api/conversations/{id}/storage/recover` with
+`{"acknowledge_unknown_outcomes": true}`, then separately call the conversation's
+`run` endpoint. Recovery records missing outcomes as unknown; it does not execute
+the original tools again. Ambiguous branches require manual inspection.
+
+If a restart finds events beyond the saved branch head, recovery does not guess
+which branch to select. Inspect the events and pass an explicit `head_event_id`
+along with `acknowledge_unknown_outcomes=True` (or both fields in the HTTP body).
+A normal capacity pause preserves the current branch head.
+If no write failed, `recover_storage()` also preserves the execution status;
+an idle conversation remains idle. Actual write-failure recovery leaves it paused.
+Neither form of recovery starts a run automatically. The HTTP endpoint returns
+`409` if the run is active or further review is needed, and `507` if storage is
+still unavailable.
+
+Creating or reopening a protected `LocalConversation` also checks capacity. If
+free space is still below the threshold, that operation fails closed. You can
+inspect existing event files without creating a conversation or starting an agent:
+
+```python
+from pathlib import Path
+
+from openhands.sdk.conversation.event_store import EventLog
+from openhands.sdk.io import LocalFileStore
+
+# Use the existing directory containing base_state.json and events/.
+conversation_dir = Path("/path/to/.conversations/")
+if not conversation_dir.is_dir():
+ raise FileNotFoundError(conversation_dir)
+
+events = EventLog(LocalFileStore(str(conversation_dir)))
+for event in events:
+ print(event.id, event.kind, event.parent_id)
+```
+
+This snippet reads records without running tools or deleting files. It does not
+guarantee the underlying filesystem remains readable after an I/O failure. Missing
+or corrupt records need inspection before recovery. Python `RemoteConversation`
+also exposes `recover_storage(acknowledge_unknown_outcomes=True,
+head_event_id=...)`, forwarding recovery to the server.
+
+See the [runnable example](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/notes_retrieval/main.py)
+and [conversation persistence guide](/sdk/guides/convo-persistence).