Scope settings snapshots to prompt builds
Reuse one normalized settings snapshot only while Agent.prepare_prompt builds a prompt, then restore normalize-on-read behavior immediately. Keep the canonical get_settings function unchanged while prompt-building callers explicitly avoid repeated Git and default resolution.
Alessandro committed
Aug 23, 2026 at 04:52 UTC
d1a9bfa13e220d449de04f2d4c354a32505cc5f7
10 files changed
+139
-8
extensions/python/_functions/AGENTS.md
+1
@@ -17,6 +17,7 @@
17
- Preserve ordering prefixes where exception handling, watchdog registration, or cleanup depends on them.
18
- Hooks that mirror persisted AI responses into UI logs must reuse existing stream log items and avoid duplicating live response-tool logs.
19
- Recovery-loop circuit breakers must stop at the General Settings limit and render their user-visible cost warning from a core framework prompt.
20
+- Prompt settings snapshots must be task-local, accessed through `get_settings_for_prompt()`, and end with the matching `Agent.prepare_prompt` call, including exceptional exits.
21
22
## Work Guidance
23
extensions/python/_functions/agent/Agent/prepare_prompt/end/_00_restore_settings.py
new
+12
@@ -0,0 +1,12 @@
1
+from helpers import settings
2
+from helpers.extension import Extension
3
+
4
+
5
+_TOKEN_KEY = "_prompt_settings_snapshot_token"
6
+
7
+
8
+class RestorePromptSettings(Extension):
9
+ def execute(self, data: dict | None = None, **kwargs):
10
+ token = data.pop(_TOKEN_KEY, None) if isinstance(data, dict) else None
11
+ if token is not None:
12
+ settings.end_prompt_settings_snapshot(token)
extensions/python/_functions/agent/Agent/prepare_prompt/start/_99_snapshot_settings.py
new
+11
@@ -0,0 +1,11 @@
1
+from helpers import settings
2
+from helpers.extension import Extension
3
+
4
+
5
+_TOKEN_KEY = "_prompt_settings_snapshot_token"
6
+
7
+
8
+class SnapshotPromptSettings(Extension):
9
+ def execute(self, data: dict | None = None, **kwargs):
10
+ if isinstance(data, dict):
11
+ data[_TOKEN_KEY] = settings.begin_prompt_settings_snapshot()
extensions/python/message_loop_prompts_after/_75_include_workdir_extras.py
+1
-1
@@ -38,7 +38,7 @@ class IncludeWorkdirExtras(Extension):
38
39
file_structure = projects.get_file_structure(project_name)
40
else:
41
- set = settings.get_settings()
41
+ set = settings.get_settings_for_prompt()
42
enabled = bool(set["workdir_show"])
43
44
if not enabled:
extensions/python/system_prompt/_13_secrets_prompt.py
+2
-2
@@ -23,11 +23,11 @@ class SecretsPrompt(Extension):
23
async def build_prompt(agent: Agent) -> str:
24
try:
25
from helpers.secrets import get_secrets_manager
26
- from helpers.settings import get_settings
26
+ from helpers.settings import get_settings_for_prompt
27
28
secrets_manager = get_secrets_manager(agent.context)
29
secrets = secrets_manager.get_secrets_for_prompt()
30
- variables = get_settings()["variables"]
30
+ variables = get_settings_for_prompt()["variables"]
31
return agent.read_prompt(
32
"agent.system.secrets.md", secrets=secrets, vars=variables
33
)
helpers/settings.py
+23
-1
@@ -1,4 +1,6 @@
1
import base64
2
+from contextvars import ContextVar, Token
3
+from copy import deepcopy
4
import hashlib
5
import json
6
import os
@@ -175,6 +177,9 @@ UI_CONTROL_VISIBILITY_DEFAULTS = {
177
SETTINGS_FILE = files.get_abs_path("usr/settings.json")
178
_settings: Settings | None = None
179
_runtime_settings_snapshot: Settings | None = None
180
+_prompt_settings_snapshot: ContextVar[Settings | None] = ContextVar(
181
+ "prompt_settings_snapshot", default=None
182
+)
183
184
OptionT = TypeVar("OptionT", bound=FieldOption)
185
@@ -375,10 +380,27 @@ def get_settings() -> Settings:
380
return norm
381
382
383
+def get_settings_for_prompt() -> Settings:
384
+ if (snapshot := _prompt_settings_snapshot.get()) is not None:
385
+ return deepcopy(snapshot)
386
+ return get_settings()
387
+
388
+
389
+def begin_prompt_settings_snapshot() -> Token:
390
+ return _prompt_settings_snapshot.set(get_settings())
391
+
392
+
393
+def end_prompt_settings_snapshot(token: Token) -> None:
394
+ _prompt_settings_snapshot.reset(token)
395
+
396
+
397
def reload_settings() -> Settings:
398
global _settings
399
_settings = None
381
- return get_settings()
400
+ current = get_settings()
401
+ if _prompt_settings_snapshot.get() is not None:
402
+ _prompt_settings_snapshot.set(deepcopy(current))
403
+ return current
404
405
406
def set_runtime_settings_snapshot(settings: Settings) -> None:
helpers/settings.py.dox.md
+5
-1
@@ -62,9 +62,13 @@
62
63
## Key Concepts
64
65
-- Important called helpers/classes observed in the source: `TypeVar`, `files.get_abs_path`, `dotenv.get_dotenv_value`, `opts.insert`, `str.strip`, `_is_valid_timezone`, `str.strip.lower`, `_normalize_timezone_setting`, `SettingsOutput`, `get_default_settings`, `_ensure_option_present`, `_resolve_runtime_timezone`, `get_default_secrets_manager`, `get_settings`, `normalize_settings`, `_load_sensitive_settings`, `settings.copy`, `_write_settings_file`, `reload_settings`, `set_settings`, `initialize_agent`.
65
+- Important called helpers/classes observed in the source: `TypeVar`, `files.get_abs_path`, `dotenv.get_dotenv_value`, `opts.insert`, `str.strip`, `_is_valid_timezone`, `str.strip.lower`, `_normalize_timezone_setting`, `SettingsOutput`, `get_default_settings`, `_ensure_option_present`, `_resolve_runtime_timezone`, `get_default_secrets_manager`, `get_settings`, `get_settings_for_prompt`, `normalize_settings`, `_load_sensitive_settings`, `deepcopy`, `settings.copy`, `_write_settings_file`, `reload_settings`, `set_settings`, `initialize_agent`.
66
- Applying settings refreshes active context configs while preserving each subordinate agent's own profile.
67
- Applying settings starts a deferred `MCPConfig.update(...)` with the current `mcp_servers` string when global MCP server settings change.
68
+- `get_settings()` retains normalize-on-read behavior. Prompt-building callers
69
+ explicitly use `get_settings_for_prompt()` to reuse one task-local snapshot
70
+ within each `Agent.prepare_prompt()` call.
71
+- Explicit reloads also refresh an active prompt snapshot.
72
- `max_consecutive_unusable_responses` defaults to `5` and controls the cost circuit breaker for malformed or repeated main-model outputs.
73
- `ui_control_visibility` stores validated mobile and desktop visibility flags for the project selector, clock, connection status, and right canvas rail; missing or malformed values fall back per device.
74
- The Global default-profile selector lists only globally available profiles.
plugins/_promptinclude/extensions/python/system_prompt/_16_promptinclude.py
+2
-2
@@ -1,7 +1,7 @@
1
from helpers.extension import Extension
2
from helpers import plugins, files, runtime
3
from helpers import projects
4
-from helpers.settings import get_settings
4
+from helpers.settings import get_settings_for_prompt
5
from agent import Agent, LoopData
6
7
from plugins._promptinclude.helpers.scanner import scan_promptinclude_files, ScanResult
@@ -61,7 +61,7 @@ def _resolve_workdir(agent: Agent) -> str:
61
if runtime.is_development():
62
folder = files.normalize_a0_path(folder)
63
return folder
64
- return get_settings()["workdir_path"]
64
+ return get_settings_for_prompt()["workdir_path"]
65
66
67
def _format_includes(agent: Agent, result: ScanResult) -> str:
prompts/agent.system.main.tips.py
+1
-1
@@ -19,6 +19,6 @@ class WorkdirPath(VariablesPlugin):
19
# folder = files.normalize_a0_path(folder)
20
# return {"workdir_path": folder}
21
22
- set = settings.get_settings()
22
+ set = settings.get_settings_for_prompt()
23
return {"workdir_path": set["workdir_path"]}
24
tests/test_settings_cache.py
new
+81
@@ -0,0 +1,81 @@
1
+import copy
2
+
3
+from helpers import extension, settings
4
+
5
+
6
+def test_settings_snapshot_is_limited_to_one_prompt(monkeypatch):
7
+ configured = settings.get_default_settings()
8
+ configured["api_keys"] = {"provider": "secret"}
9
+ versions = iter(["first", "second", "third", "fourth"])
10
+ calls = 0
11
+
12
+ def defaults():
13
+ nonlocal calls
14
+ calls += 1
15
+ result = copy.deepcopy(configured)
16
+ result["version"] = next(versions)
17
+ return result
18
+
19
+ monkeypatch.setattr(settings, "_settings", configured)
20
+ monkeypatch.setattr(settings, "_read_settings_file", lambda: configured)
21
+ monkeypatch.setattr(settings, "get_default_settings", defaults)
22
+ monkeypatch.setattr(settings, "_load_sensitive_settings", lambda _value: None)
23
+
24
+ token = settings.begin_prompt_settings_snapshot()
25
+ try:
26
+ configured["workdir_show"] = False
27
+ first = settings.get_settings_for_prompt()
28
+ second = settings.get_settings_for_prompt()
29
+
30
+ assert calls == 1
31
+ assert first == second
32
+ assert first["workdir_show"] is True
33
+ assert first is not second
34
+ assert first["api_keys"] is not second["api_keys"]
35
+
36
+ first["api_keys"]["provider"] = "masked"
37
+ assert settings.get_settings_for_prompt()["api_keys"]["provider"] == "secret"
38
+
39
+ current = settings.get_settings()
40
+ assert current["version"] == "second"
41
+ assert current["workdir_show"] is False
42
+ assert settings.get_settings_for_prompt()["workdir_show"] is True
43
+
44
+ reloaded = settings.reload_settings()
45
+ assert reloaded["version"] == "third"
46
+ assert reloaded["workdir_show"] is False
47
+ assert settings.get_settings_for_prompt() == reloaded
48
+ finally:
49
+ settings.end_prompt_settings_snapshot(token)
50
+
51
+ refreshed = settings.get_settings()
52
+ assert refreshed["workdir_show"] is False
53
+ assert refreshed["version"] == "fourth"
54
+ assert calls == 4
55
+
56
+
57
+def test_prompt_snapshot_hooks_are_registered_and_paired():
58
+ start = next(
59
+ cls
60
+ for cls in extension._get_extension_classes( # type: ignore[attr-defined]
61
+ "_functions/agent/Agent/prepare_prompt/start"
62
+ )
63
+ if cls.__name__ == "SnapshotPromptSettings"
64
+ )
65
+ end = next(
66
+ cls
67
+ for cls in extension._get_extension_classes( # type: ignore[attr-defined]
68
+ "_functions/agent/Agent/prepare_prompt/end"
69
+ )
70
+ if cls.__name__ == "RestorePromptSettings"
71
+ )
72
+ previous = settings._prompt_settings_snapshot.get()
73
+ data = {}
74
+
75
+ start(agent=None).execute(data=data)
76
+ try:
77
+ assert settings._prompt_settings_snapshot.get() is not None
78
+ finally:
79
+ end(agent=None).execute(data=data)
80
+
81
+ assert settings._prompt_settings_snapshot.get() is previous