Is there an existing issue for the same bug?
Bug Description
RemoteWorkspace.load_skills_from_agent_server() in openhands-sdk/openhands/sdk/workspace/remote/base.py always constructs a brand-new AgentContext from scratch when it returns, instead of updating any AgentContext the caller already had. Every field other than skills and load_public_skills is silently reset to its class default — load_user_skills, load_project_skills, disabled_skills, marketplace_path, system_message_suffix, and user_message_suffix all disappear from the returned context regardless of what the caller had configured.
One instance of this is worth calling out specifically: current_datetime defaults to default_factory=lambda: datetime.now().astimezone(), not None. So a caller's AgentContext(current_datetime=<some fixed value>) doesn't just get dropped — it gets silently replaced with a freshly-generated "now" timestamp at whatever moment load_skills_from_agent_server() happened to run.
Confirmed still present on main at the two AgentContext(...) construction lines near the end of the function (currently ~lines 928–933):
if loaded_skills:
agent_context = AgentContext(skills=loaded_skills, load_public_skills=False)
else:
logger.warning("No skills loaded, falling back to public skills")
agent_context = AgentContext(skills=[], load_public_skills=True)
Originally flagged as an out-of-scope "optional companion change" in #4542 while fixing the load_memory propagation bug (#4566) — noted there as worth doing on its own merits but an independent code path from that fix.
Note:
OpenHandsCloudWorkspace in openhands-workspace/openhands/workspace/cloud/workspace.py re-declares this method's full signature and forwards every argument to super().load_skills_from_agent_server(...) — done deliberately so griffe (this repo's API-compatibility checker) doesn't flag inherited-method removal from the subclass. That means any fix to the base method's signature must also be mirrored here, or OpenHandsCloudWorkspace callers silently never get access to the new capability while its docstring still claims parity with RemoteWorkspace.
Expected Behavior
load_skills_from_agent_server() should preserve any AgentContext fields the caller already set — marketplace_path, disabled_skills, message suffixes, current_datetime, etc. — and only change the two fields the function itself is responsible for: skills (the whole point of the call) and load_public_skills (set by the function's own found-vs-fallback logic). Accepting an optional base context and returning base_context.model_copy(update={"skills": ..., "load_public_skills": ...}) instead of constructing a fresh AgentContext(...) would make the helper non-destructive. AgentContext isn't a frozen model, so nothing forces this mechanically — it's the same convention the SDK already uses elsewhere for non-mutating updates on value-like objects, applied here for the same reason: avoid the caller having to know their prior configuration was reconstructed rather than preserved.
Actual Behavior
The reset is reproducible without a running agent-server by mocking the one network call (_call_skills_api) the function makes internally. Save this as repro_load_skills_context.py:
import time
from unittest.mock import patch
from openhands.sdk import RemoteWorkspace
workspace = RemoteWorkspace(
host="https://agent-server.example.com", working_dir="/workspace"
)
with patch.object(
workspace,
"_call_skills_api",
return_value=[{"name": "demo-skill", "content": "demo"}],
):
_, context_1 = workspace.load_skills_from_agent_server()
time.sleep(1.5)
_, context_2 = workspace.load_skills_from_agent_server()
print("marketplace_path:", context_1.marketplace_path)
print("disabled_skills:", context_1.disabled_skills)
print("current_datetime (call 1):", context_1.current_datetime)
print("current_datetime (call 2):", context_2.current_datetime)
print("Same timestamp across calls?", context_1.current_datetime == context_2.current_datetime)
Run with:
uv run python repro_load_skills_context.py
Observed output:
[09/06/26 09:50:59] INFO Loading skills via agent-server... base.py:907
[09/06/26 09:50:59] INFO Loaded 1 skills base.py:924
[09/06/26 09:51:01] INFO Loading skills via agent-server... base.py:907
[09/06/26 09:51:01] INFO Loaded 1 skills base.py:924
marketplace_path: marketplaces/default.json
disabled_skills: []
current_datetime (call 1): 2026-09-06 09:50:59.622681+05:30
current_datetime (call 2): 2026-09-06 09:51:01.129209+05:30
Same timestamp across calls? False
There is no parameter on load_skills_from_agent_server() today that can change marketplace_path or disabled_skills away from their class defaults — the function offers no way to preserve them. And current_datetime differing between two calls 1.5 seconds apart, with no caller input at all, is direct proof it is being freshly generated on every call rather than sourced from any prior context.
Acceptance Criteria
Screenshots and Additional Context
Related: #4542 (root issue), #4566 (merged fix for the load_memory propagation half of that issue — this is the other half, called out there as independent and deferred).
I'd like to work on this — I'll open a PR shortly with regression tests covering the acceptance criteria above.
Is there an existing issue for the same bug?
Bug Description
RemoteWorkspace.load_skills_from_agent_server()inopenhands-sdk/openhands/sdk/workspace/remote/base.pyalways constructs a brand-newAgentContextfrom scratch when it returns, instead of updating anyAgentContextthe caller already had. Every field other thanskillsandload_public_skillsis silently reset to its class default —load_user_skills,load_project_skills,disabled_skills,marketplace_path,system_message_suffix, anduser_message_suffixall disappear from the returned context regardless of what the caller had configured.One instance of this is worth calling out specifically:
current_datetimedefaults todefault_factory=lambda: datetime.now().astimezone(), notNone. So a caller'sAgentContext(current_datetime=<some fixed value>)doesn't just get dropped — it gets silently replaced with a freshly-generated "now" timestamp at whatever momentload_skills_from_agent_server()happened to run.Confirmed still present on
mainat the twoAgentContext(...)construction lines near the end of the function (currently ~lines 928–933):Originally flagged as an out-of-scope "optional companion change" in #4542 while fixing the
load_memorypropagation bug (#4566) — noted there as worth doing on its own merits but an independent code path from that fix.Note:
OpenHandsCloudWorkspaceinopenhands-workspace/openhands/workspace/cloud/workspace.pyre-declares this method's full signature and forwards every argument tosuper().load_skills_from_agent_server(...)— done deliberately sogriffe(this repo's API-compatibility checker) doesn't flag inherited-method removal from the subclass. That means any fix to the base method's signature must also be mirrored here, orOpenHandsCloudWorkspacecallers silently never get access to the new capability while its docstring still claims parity withRemoteWorkspace.Expected Behavior
load_skills_from_agent_server()should preserve anyAgentContextfields the caller already set —marketplace_path,disabled_skills, message suffixes,current_datetime, etc. — and only change the two fields the function itself is responsible for:skills(the whole point of the call) andload_public_skills(set by the function's own found-vs-fallback logic). Accepting an optional base context and returningbase_context.model_copy(update={"skills": ..., "load_public_skills": ...})instead of constructing a freshAgentContext(...)would make the helper non-destructive.AgentContextisn't a frozen model, so nothing forces this mechanically — it's the same convention the SDK already uses elsewhere for non-mutating updates on value-like objects, applied here for the same reason: avoid the caller having to know their prior configuration was reconstructed rather than preserved.Actual Behavior
The reset is reproducible without a running agent-server by mocking the one network call (
_call_skills_api) the function makes internally. Save this asrepro_load_skills_context.py:Run with:
Observed output:
There is no parameter on
load_skills_from_agent_server()today that can changemarketplace_pathordisabled_skillsaway from their class defaults — the function offers no way to preserve them. Andcurrent_datetimediffering between two calls 1.5 seconds apart, with no caller input at all, is direct proof it is being freshly generated on every call rather than sourced from any prior context.Acceptance Criteria
load_skills_from_agent_server()accepts an optional baseAgentContextinstead of always constructing one from scratchskillsandload_public_skills(which the function's found/fallback logic continues to set as it does today)current_datetimefrom the base context is preserved, not regeneratedcurrent_datetimespecifically survives a round-tripOpenHandsCloudWorkspace.load_skills_from_agent_server()(openhands-workspace/openhands/workspace/cloud/workspace.py) accepts and forwardsbase_contexttosuper().load_skills_from_agent_server(...), with test coverage confirming preserved fields survive through the overrideScreenshots and Additional Context
Related: #4542 (root issue), #4566 (merged fix for the
load_memorypropagation half of that issue — this is the other half, called out there as independent and deferred).I'd like to work on this — I'll open a PR shortly with regression tests covering the acceptance criteria above.