Skip to content

Commit e8d664b

Browse files
authored
Merge pull request #58 from ApodexAI/fix/spill-scope-recovery
fix(spill): one store and one scope key for spill storage, mount and reads
2 parents 8e0e811 + 89875b2 commit e8d664b

17 files changed

Lines changed: 867 additions & 329 deletions

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ Initial open-source release of FrontierAgent.
7676
`reboot` are no longer refused, while `bash -c`, shell heredocs, pipes into a
7777
shell, evaluators (`watch`/`tmux`/…) and `systemctl` shutdown units still are.
7878
`DROP TABLE` keeps screening the whole text.
79+
- Spill recovery: oversized and compacted tool results are stored through
80+
AgentCore's `SpillStore` (one store, one registry). A single scope key
81+
(`task:llm_session`) now decides the store directory, the bwrap `/spill` mount
82+
and read authorization: a jail sees only its own store and its sub-agents',
83+
siblings and other conversations see nothing, and `_path_auth` no longer
84+
authorizes every store the process created. A path is advertised only when
85+
the backend actually running commands can open it (`auto` going to E2B, or
86+
bwrap unusable, gets none), and the compaction spill callback is withheld
87+
then. Compacting a truncated preview points at the original full body
88+
instead of storing the preview as "[Full text]". The `recover_result` footer
89+
is only shown when a trajectory JSONL exists. Known limitation: `container`
90+
mode without the inner bwrap jail shares one tool uid, so model commands can
91+
still read other scopes there.
7992
- Surface finalize-gate bypasses on the final turn: an answer delivered despite
8093
open task-board items now carries an unfinished-work note and a
8194
`finalize_gate_bypassed` marker instead of reading as a clean success.

‎apodex/tests/test_changes.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,8 @@ def test_clear_discards_only_the_active_sessions_spill(tmp_path, monkeypatch):
11351135
from apodex.session import TerminalSession
11361136
from frontier_agent.core.messages import user_msg
11371137

1138+
# This tests store access/cleanup, independently of host bwrap support.
1139+
monkeypatch.setenv("SANDBOX_BACKEND", "native")
11381140
monkeypatch.setenv("HOME", str(tmp_path))
11391141
monkeypatch.setenv("APODEX_RUNS_ROOT", str(tmp_path / "runs"))
11401142
monkeypatch.delenv("APODEX_INPUT_STAGING_DIR", raising=False)
@@ -1239,6 +1241,8 @@ def test_context_reset_still_cleans_a_session_private_workspace(tmp_path, monkey
12391241
private.mkdir(parents=True)
12401242
link = tmp_path / "current-workspace"
12411243
link.symlink_to(private, target_is_directory=True)
1244+
# This tests store access/cleanup, independently of host bwrap support.
1245+
monkeypatch.setenv("SANDBOX_BACKEND", "native")
12421246
monkeypatch.setenv("APODEX_RUNS_ROOT", str(runs_root))
12431247
monkeypatch.setenv("FRONTIER_AGENT_WORKSPACE_DIR", str(link))
12441248
monkeypatch.setenv("APODEX_SPILL_DIR", str(tmp_path / "store"))

‎apodex/tests/test_tui.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3159,7 +3159,8 @@ async def test_user_scroll_can_leave_and_restore_stream_following() -> None:
31593159
await pilot.pause()
31603160
assert transcript.scroll_offset.y == 5
31613161

3162-
transcript.scroll_end(animate=False)
3162+
# Assert the completed scroll, without depending on a deferred refresh.
3163+
transcript.scroll_end(animate=False, immediate=True)
31633164
await pilot.pause()
31643165
assert transcript._wants_tail is True
31653166
app.sink.content_delta("tail output\n\n" * 20)

‎frontier_agent/core/runtime/loop/agent_loop.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from frontier_agent.core.execution_context import (
2222
ExecutionScope,
2323
chain_fallback_active,
24+
get_current_execution_scope,
2425
reset_current_execution_scope,
2526
set_current_execution_scope,
2627
)
@@ -49,13 +50,26 @@ def _enter_scope(
4950
phase_id=phase_id,
5051
metadata=metadata,
5152
)
53+
# A loop entered from inside another (an in-process sub-agent) is that
54+
# loop's child: the parent may read the child's spill store, siblings not.
55+
from plugins.tools._overflow import register_child_scope
56+
57+
register_child_scope(get_current_execution_scope(), scope)
5258
return scope, set_current_execution_scope(scope)
5359

5460

5561
def _body_has_spill_reference(body: str) -> bool:
62+
"""Whether the trajectory recovery footer should stay quiet for ``body``.
63+
64+
True when the body already names a spill file (that file holds the full
65+
output), and also when this agent has no trajectory JSONL: the footer names
66+
``recover_result``, which reads exactly that file, so without one it would
67+
hand the model a handle that can only answer "unavailable".
68+
"""
5669
from plugins.tools._overflow import body_names_a_spill_file
70+
from plugins.tools.recover_result import trajectory_recovery_available
5771

58-
return body_names_a_spill_file(body)
72+
return body_names_a_spill_file(body) or not trajectory_recovery_available()
5973

6074

6175
def _with_recovery_handle(body: str, result: Any, turn: int, *, enabled: bool) -> str:

0 commit comments

Comments
 (0)