diff --git a/.proofs/2026-06-18-pawfs-direct-key-write-hotfix.md b/.proofs/2026-06-18-pawfs-direct-key-write-hotfix.md
new file mode 100644
index 00000000..0e2d84a6
--- /dev/null
+++ b/.proofs/2026-06-18-pawfs-direct-key-write-hotfix.md
@@ -0,0 +1,57 @@
+# PawFS direct-key write hotfix proof
+
+Date: 2026-06-18
+
+## Scope
+
+Live Katagami regeneration reached `temper.write`, then failed on PawFS directory
+resolution because Monty looked up directories with a broad OData collection
+filter:
+
+`/tdata/Directories?$filter=Path eq ... and WorkspaceId eq ...`
+
+Production rejected that shape with `413 QueryTooLarge` once the directory table
+was large enough. This patch keeps the existing PawFS entity model and changes
+Monty write resolution to derive deterministic directory/file IDs from
+`(workspace_id, normalized_path)`, then read by key before creating missing
+entities.
+
+## ADR judgement
+
+No new ADR. This is a scoped implementation repair under the existing PawFS
+direct-access model, not a new entity type, workflow, storage model, trigger,
+policy boundary, or agent capability surface.
+
+## Red
+
+Added `monty_pawfs_write_path_uses_direct_keys_not_broad_path_filters` in
+`crates/temperpaw/tests/paw_fs_hot_path.rs`.
+
+Initial result before implementation:
+
+```text
+cargo test -p temperpaw --test paw_fs_hot_path monty_pawfs_write_path_uses_direct_keys_not_broad_path_filters
+test monty_pawfs_write_path_uses_direct_keys_not_broad_path_filters ... FAILED
+Monty PawFS writes should derive deterministic directory ids
+```
+
+## Green
+
+Local verification after implementation:
+
+```text
+cargo test -p temperpaw --test paw_fs_hot_path
+test result: ok. 14 passed; 0 failed
+
+cargo test --manifest-path os-apps/paw-agent/wasm/monty_repl/Cargo.toml
+test result: ok. 73 passed; 0 failed
+
+./os-apps/paw-agent/wasm/build.sh
+All WASM modules built, including rebuilt monty_repl.
+```
+
+## Live E2E
+
+Pending. Next step is to publish the new `temperpaw/paw-agent` Genesis ref,
+update/reinstall OpenPaw production, and rerun the live Katagami regeneration
+job that reproduced the PawFS write-path 413.
diff --git a/crates/temperpaw/tests/paw_fs_hot_path.rs b/crates/temperpaw/tests/paw_fs_hot_path.rs
index 1495e8bc..85538090 100644
--- a/crates/temperpaw/tests/paw_fs_hot_path.rs
+++ b/crates/temperpaw/tests/paw_fs_hot_path.rs
@@ -104,6 +104,32 @@ fn monty_exposes_write_many_for_artifact_sets() {
);
}
+#[test]
+fn monty_pawfs_write_path_uses_direct_keys_not_broad_path_filters() {
+ let source = repo_file("os-apps/paw-agent/wasm/monty_repl/src/entity_ops.rs");
+
+ assert!(
+ source.contains("pawfs_directory_id"),
+ "Monty PawFS writes should derive deterministic directory ids"
+ );
+ assert!(
+ source.contains("pawfs_file_id"),
+ "Monty PawFS writes should derive deterministic file ids"
+ );
+ assert!(
+ source.contains("/tdata/Directories('{directory_id}')"),
+ "Monty PawFS writes should read directories by key"
+ );
+ assert!(
+ source.contains("/tdata/Files('{file_id}')"),
+ "Monty PawFS writes should read files by key"
+ );
+ assert!(
+ !source.contains("Path eq '{}' and WorkspaceId eq '{}' and Status ne 'Archived'"),
+ "Monty PawFS writes must not use broad Path+Workspace collection filters that hit bounded OData candidate limits"
+ );
+}
+
#[test]
fn default_agent_tool_allowlists_include_write_many() {
for path in [
diff --git a/crates/temperpaw/tests/session_turn_architecture.rs b/crates/temperpaw/tests/session_turn_architecture.rs
index b45e1b7d..26c90fda 100644
--- a/crates/temperpaw/tests/session_turn_architecture.rs
+++ b/crates/temperpaw/tests/session_turn_architecture.rs
@@ -359,6 +359,10 @@ fn session_entry_readbacks_stay_within_bounded_query_budget() {
let root = repo_root();
let helpers = fs::read_to_string(root.join("os-apps/paw-agent/wasm/wasm-helpers/src/lib.rs"))
.expect("wasm helpers source should exist");
+ let context_preparer_wasm =
+ fs::read(root.join("os-apps/paw-agent/wasm/context_preparer/context_preparer.wasm"))
+ .expect("context_preparer.wasm should be checked in");
+ let context_preparer_wasm = String::from_utf8_lossy(&context_preparer_wasm);
let route_message =
fs::read_to_string(root.join("os-apps/paw-channels/wasm/route_message/src/lib.rs"))
.expect("route_message source should exist");
@@ -375,6 +379,32 @@ fn session_entry_readbacks_stay_within_bounded_query_budget() {
helpers.contains("session_entries_verify_urls"),
"batched SessionEntry readback should use one bounded per-entry URL per expected entry"
);
+ assert!(
+ helpers.contains("read_session_entries_from_leaf"),
+ "SessionEntry history reads should prefer bounded direct EntryId parent-chain reads from session_leaf_id"
+ );
+ assert!(
+ helpers.contains("ParentEntryId"),
+ "SessionEntry parent-chain reads must follow ParentEntryId instead of session-wide scans"
+ );
+ assert!(
+ context_preparer_wasm.contains("SessionEntry direct lookup failed")
+ && context_preparer_wasm.contains("SessionEntry parent-chain read")
+ && context_preparer_wasm.contains("SessionEntry virtual first-turn leaf"),
+ "built context_preparer.wasm must include the bounded leaf parent-chain reader"
+ );
+ assert!(
+ helpers.contains("$orderby=Sequence%20asc"),
+ "SessionEntry history reads should use deterministic keyset ordering"
+ );
+ assert!(
+ helpers.contains("Sequence%20ge%20{next_sequence}"),
+ "SessionEntry history reads should continue by Sequence instead of broad skip scans"
+ );
+ assert!(
+ !helpers.contains("&$skip="),
+ "SessionEntry history reads must not use $skip because production bounded OData can reject high-candidate scans"
+ );
assert!(
route_message.contains("session_leaf_id is missing; starting clean continuation"),
"route_message should start cleanly instead of broad-scanning when the prior leaf hint is missing"
diff --git a/os-apps/paw-agent/wasm/monty_repl/src/entity_ops.rs b/os-apps/paw-agent/wasm/monty_repl/src/entity_ops.rs
index 3cf8d554..cc7e986b 100644
--- a/os-apps/paw-agent/wasm/monty_repl/src/entity_ops.rs
+++ b/os-apps/paw-agent/wasm/monty_repl/src/entity_ops.rs
@@ -2176,6 +2176,30 @@ fn http_get(
.map_err(|e| format!("failed to parse response from {path}: {e}"))
}
+fn http_get_optional(
+ ctx: &Context,
+ api_url: &str,
+ _tenant: &str,
+ _principal_id: &str,
+ path: &str,
+) -> Result