feat(context): per-scope instruction budgets with visible truncation (#7086)

Thanks @7jrxt42BxFZo4iAnN4CX for the design and implementation of per-scope instruction budgets (#6526).
This commit is contained in:
Hunter Bown authored and GitHub committed 2026-10-27 14:26:50 +08:00
commit 5c3d461d8a
36 files changed
+4433 -805

No files matched your search

+10
View File
@@ -48,6 +48,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
cancellation wiring stays in place; its stale unused-code suppressions are cancellation wiring stays in place; its stale unused-code suppressions are
removed. No supported command or feature is retired by this cleanup. removed. No supported command or feature is retired by this cleanup.
- Standing instructions are budgeted per scope (`global` and `project`) in
estimated tokens instead of one 48 KiB byte ceiling shared by everything, and
every trim, skip, and refusal is reported — as a system cell at session start,
on stderr in `exec`, to the model as a runtime-provenance message, and per
source in `/context report`. `AGENTS.md` still wins; a directory without one
now falls back to its `CLAUDE.md` (`claude_fallback = false` opts out).
Configured `instructions = [...]` entries are charged to the global scope.
New `[instruction_budget]` table (user config only) with per-scope token
budgets, percentages of the usable window, `file_max_bytes`, `policy`, the
static-prefix guard, and `claude_fallback` (#6526).
- Local Ollama defaults now come from chat-capable tags, and `/provider ollama` - Local Ollama defaults now come from chat-capable tags, and `/provider ollama`
refreshes the live catalog before switching. Exec and Runtime API defaults use refreshes the live catalog before switching. Exec and Runtime API defaults use
the same discovery path. Fresh context observations come from loaded models' the same discovery path. Fresh context observations come from loaded models'
+18 -5
View File
@@ -168,11 +168,24 @@ sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-acc
# "muse", or "all". Env: CODEWHALE_PROJECT_INSTRUCTION_IMPORTS (comma-separated). # "muse", or "all". Env: CODEWHALE_PROJECT_INSTRUCTION_IMPORTS (comma-separated).
# project_instruction_imports = ["claude"] # project_instruction_imports = ["claude"]
# #
# Everything that reaches the model as standing project instruction authority — # Standing instructions are budgeted per scope, in estimated tokens, and every
# the repository-root -> workspace AGENTS.md chain, the global fallback layer, # trim or skip is reported at session start, to the model, and in /context.
# .codewhale/rules/*.md, and any imported foreign files — shares one 48 KiB # `global` covers ~/.codewhale/AGENTS.md (or ~/.agents/AGENTS.md), the global
# aggregate budget. Instructions claim it first and are trimmed from the # instructions.md fallbacks, ~/.claude/CLAUDE.md (last resort) and the
# broadest scope inward, so the nearest-scope file is the last thing dropped. # `instructions = [...]` array. `project` covers the repository-root ->
# workspace AGENTS.md chain, co-read foreign files, and .codewhale/rules/*.md.
# A scope is capped at the smaller of its absolute token budget and its share
# of the usable window (context window minus the response ceiling and a 10%
# safety reserve). User config only: a cloned repository cannot change it.
# [instruction_budget]
# global_budget_tokens = 100000 # positive; absolute cap for the global scope
# project_budget_tokens = 100000 # positive; absolute cap for the project scope
# global_budget_percent = 25 # 1..=100, share of the usable window
# project_budget_percent = 25 # 1..=100, share of the usable window
# static_prefix_guard_percent = 80 # 1..=100; guard must leave >=10 points
# file_max_bytes = 8388608 # hard per-file ceiling; larger files skip
# policy = "trim" # trim | refuse
# claude_fallback = true # AGENTS.md wins; else CLAUDE.md
# prompt_suggestion = true # opt-in: show ghost-text follow-up question in composer after each turn # prompt_suggestion = true # opt-in: show ghost-text follow-up question in composer after each turn
# Optional tab/window title shown as `[title] …` in front of the terminal # Optional tab/window title shown as `[title] …` in front of the terminal
+10
View File
@@ -48,6 +48,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
cancellation wiring stays in place; its stale unused-code suppressions are cancellation wiring stays in place; its stale unused-code suppressions are
removed. No supported command or feature is retired by this cleanup. removed. No supported command or feature is retired by this cleanup.
- Standing instructions are budgeted per scope (`global` and `project`) in
estimated tokens instead of one 48 KiB byte ceiling shared by everything, and
every trim, skip, and refusal is reported — as a system cell at session start,
on stderr in `exec`, to the model as a runtime-provenance message, and per
source in `/context report`. `AGENTS.md` still wins; a directory without one
now falls back to its `CLAUDE.md` (`claude_fallback = false` opts out).
Configured `instructions = [...]` entries are charged to the global scope.
New `[instruction_budget]` table (user config only) with per-scope token
budgets, percentages of the usable window, `file_max_bytes`, `policy`, the
static-prefix guard, and `claude_fallback` (#6526).
- Local Ollama defaults now come from chat-capable tags, and `/provider ollama` - Local Ollama defaults now come from chat-capable tags, and `/provider ollama`
refreshes the live catalog before switching. Exec and Runtime API defaults use refreshes the live catalog before switching. Exec and Runtime API defaults use
the same discovery path. Fresh context observations come from loaded models' the same discovery path. Fresh context observations come from loaded models'
@@ -122,7 +122,7 @@ pub(crate) fn assert_fixture(name: &str, actual: &str) {
); );
} }
/// The captured Linux environment contributes 21 tokens to the 2367 total. /// The captured Linux environment contributes 21 tokens to the 2506 total.
/// Keep its text frozen independently of the production prompt renderer, but /// Keep its text frozen independently of the production prompt renderer, but
/// substitute real host facts: Windows/pwsh.exe contributes 22, for example. /// substitute real host facts: Windows/pwsh.exe contributes 22, for example.
/// Never derive expectations from the report being tested or erase its counts. /// Never derive expectations from the report being tested or erase its counts.
@@ -130,7 +130,7 @@ fn context_fixture_for_host(expected: &str, platform: &str, shell: &str) -> Stri
let environment = let environment =
format!("## Environment\n\n- lang: en\n- platform: {platform}\n- shell: {shell}"); format!("## Environment\n\n- lang: en\n- platform: {platform}\n- shell: {shell}");
let tokens = environment.chars().count().div_ceil(3); let tokens = environment.chars().count().div_ceil(3);
let total = 2367 - 21 + tokens; let total = 2506 - 21 + tokens;
expected expected
.replace( .replace(
"EnvironmentBlock: Runtime environment [<workspace>] - 21 tokens", "EnvironmentBlock: Runtime environment [<workspace>] - 21 tokens",
@@ -145,11 +145,11 @@ fn context_fixture_for_host(expected: &str, platform: &str, shell: &str) -> Stri
&format!("\"estimated_tokens\": {tokens},"), &format!("\"estimated_tokens\": {tokens},"),
) )
.replace( .replace(
"Source-entry total: 2367 tokens", "Source-entry total: 2506 tokens",
&format!("Source-entry total: {total} tokens"), &format!("Source-entry total: {total} tokens"),
) )
.replace( .replace(
"\"total_estimated_tokens\": 2367,", "\"total_estimated_tokens\": 2506,",
&format!("\"total_estimated_tokens\": {total},"), &format!("\"total_estimated_tokens\": {total},"),
) )
} }
@@ -290,13 +290,21 @@ fn context_expectations_preserve_counts_for_each_host() {
.replace("- 21 tokens", "- 22 tokens") .replace("- 21 tokens", "- 22 tokens")
.replace("environment (21)", "environment (22)") .replace("environment (21)", "environment (22)")
.replace("\"estimated_tokens\": 21,", "\"estimated_tokens\": 22,") .replace("\"estimated_tokens\": 21,", "\"estimated_tokens\": 22,")
.replace("2367", "2368"); .replace("2506", "2507");
let expected = context_fixture_for_host(&baseline, "windows", "pwsh.exe"); let expected = context_fixture_for_host(&baseline, "windows", "pwsh.exe");
assert_eq!(expected, windows, "{name}"); assert_eq!(expected, windows, "{name}");
assert_ne!(expected, baseline, "{name} must retain the host difference"); // A fixture that carries no host-dependent number (the top-source list
// A regression in another source or the environment count still fails. // can stop naming the environment block) is identical on both hosts, so
// only the host-carrying fixtures can prove the difference is kept.
let host_dependent = baseline.contains("- 21 tokens")
|| baseline.contains("environment (21)")
|| baseline.contains("\"estimated_tokens\": 21,");
if host_dependent {
assert_ne!(expected, baseline, "{name} must retain the host difference");
assert_ne!(expected, windows.replace("22", "23"));
}
// A regression in another source still fails on every fixture.
assert_ne!(expected, windows.replace("1803", "1804")); assert_ne!(expected, windows.replace("1803", "1804"));
assert_ne!(expected, windows.replace("22", "23"));
} }
} }
@@ -23,6 +23,16 @@ message:
"authority_tier": 5, "authority_tier": 5,
"truncation_reason": null "truncation_reason": null
}, },
{
"source_kind": "project_context",
"label": "Instruction budget",
"source_path": null,
"activation_reason": "config_enabled",
"estimated_tokens": 0,
"counting_confidence": "high",
"authority_tier": 5,
"truncation_reason": "global 0 / 100000 estimated tokens; project 139 / 100000 estimated tokens; policy trim; usable window 516000 tokens; static-prefix guard 412800 tokens"
},
{ {
"source_kind": "project_context_pack", "source_kind": "project_context_pack",
"label": "Project context pack", "label": "Project context pack",
@@ -31,6 +31,16 @@ message:
"authority_tier": 5, "authority_tier": 5,
"truncation_reason": null "truncation_reason": null
}, },
{
"source_kind": "project_context",
"label": "Instruction budget",
"source_path": null,
"activation_reason": "config_enabled",
"estimated_tokens": 0,
"counting_confidence": "high",
"authority_tier": 5,
"truncation_reason": "global 0 / 100000 estimated tokens; project 139 / 100000 estimated tokens; policy trim; usable window 516000 tokens; static-prefix guard 412800 tokens"
},
{ {
"source_kind": "project_context_pack", "source_kind": "project_context_pack",
"label": "Project context pack", "label": "Project context pack",
@@ -12,6 +12,7 @@ Manage standing law: /constitution (status/preview), /constitution repo (repo-lo
Sources: Sources:
- Constitution: Bundled constitution, language policy, and output policy [bundled in this codewhale-tui build (BASE_PROMPT, compiled in)] - 1803 tokens (High, tier 1) - Constitution: Bundled constitution, language policy, and output policy [bundled in this codewhale-tui build (BASE_PROMPT, compiled in)] - 1803 tokens (High, tier 1)
- ProjectContext: Project instructions - 160 tokens (High, tier 5) - ProjectContext: Project instructions - 160 tokens (High, tier 5)
- ProjectContext: Instruction budget - 0 tokens (High, tier 5) - global 0 / 100000 estimated tokens; project 139 / 100000 estimated tokens; policy trim; usable window 516000 tokens; static-prefix guard 412800 tokens
- ProjectContextPack: Project context pack [<workspace>] - 0 tokens (High, tier 5) - disabled; project_map provides this information on demand - ProjectContextPack: Project context pack [<workspace>] - 0 tokens (High, tier 5) - disabled; project_map provides this information on demand
- SkillsBlock: Available skills [<skills>] - 0 tokens (High, tier 5) - no skills discovered - SkillsBlock: Available skills [<skills>] - 0 tokens (High, tier 5) - no skills discovered
- ContextManagement: WORK runtime mode - 0 tokens (High, tier 3) - mode enforced by runtime policy and the live tool catalog; no prompt doctrine - ContextManagement: WORK runtime mode - 0 tokens (High, tier 3) - mode enforced by runtime policy and the live tool catalog; no prompt doctrine
+11
View File
@@ -2093,6 +2093,13 @@ pub struct Config {
/// `cline`, `windsurf`, `gemini`, `copilot`, `muse`, or `all`. /// `cline`, `windsurf`, `gemini`, `copilot`, `muse`, or `all`.
#[serde(default, alias = "projectInstructionImports")] #[serde(default, alias = "projectInstructionImports")]
pub project_instruction_imports: Vec<String>, pub project_instruction_imports: Vec<String>,
/// Per-scope standing-instruction budgets (#6526).
///
/// User config only, like `instructions`: the project-scope overlay
/// cannot set it, so a cloned repository cannot shrink the user's global
/// budget or force `policy = "refuse"`.
#[serde(default, alias = "instructionBudget")]
pub instruction_budget: Option<crate::project_context::InstructionBudgetConfig>,
/// `telemetry` as written to the config file, before environment and /// `telemetry` as written to the config file, before environment and
/// default resolution. Kept so doctor and config displays can state the /// default resolution. Kept so doctor and config displays can state the
/// *resolved* consent with its source (default | env | config) instead of /// *resolved* consent with its source (default | env | config) instead of
@@ -10087,6 +10094,10 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
// replace the instruction array. Project-scope config is filtered in // replace the instruction array. Project-scope config is filtered in
// main.rs and cannot set instruction paths. // main.rs and cannot set instruction paths.
instructions: override_cfg.instructions.or(base.instructions), instructions: override_cfg.instructions.or(base.instructions),
// #6526: the instruction-budget table is user authority for the same
// reason `instructions` is — a workspace does not get to decide how
// much of the user's context its own files may claim.
instruction_budget: override_cfg.instruction_budget.or(base.instruction_budget),
stop_words: override_cfg.stop_words.or(base.stop_words), stop_words: override_cfg.stop_words.or(base.stop_words),
allow_shell: override_cfg.allow_shell.or(base.allow_shell), allow_shell: override_cfg.allow_shell.or(base.allow_shell),
prompt_suggestion: override_cfg.prompt_suggestion.or(base.prompt_suggestion), prompt_suggestion: override_cfg.prompt_suggestion.or(base.prompt_suggestion),
+106 -7
View File
@@ -282,6 +282,7 @@ pub fn build_context_report(app: &App) -> PromptSourceMap {
app.mode, app.mode,
Some(app.plugin_registry.as_ref()), Some(app.plugin_registry.as_ref()),
Some(context_window.tokens), Some(context_window.tokens),
&[],
); );
add_app_runtime_entries(&mut builder, app); add_app_runtime_entries(&mut builder, app);
builder.finish( builder.finish(
@@ -395,6 +396,7 @@ pub fn build_headless_context_report(config: &Config, workspace: &Path) -> Promp
AppMode::Agent, AppMode::Agent,
None, None,
Some(context_window.tokens), Some(context_window.tokens),
&config.instructions_paths(),
); );
let memory_path = config.memory_path(); let memory_path = config.memory_path();
let memory_enabled = config.memory_enabled(); let memory_enabled = config.memory_enabled();
@@ -463,6 +465,7 @@ fn base_source_entries(
mode: AppMode, mode: AppMode,
plugin_registry: Option<&crate::plugins::PluginRegistry>, plugin_registry: Option<&crate::plugins::PluginRegistry>,
context_window_tokens: Option<u32>, context_window_tokens: Option<u32>,
configured_instruction_paths: &[std::path::PathBuf],
) -> ReportBuilder { ) -> ReportBuilder {
let mut builder = ReportBuilder::new(); let mut builder = ReportBuilder::new();
@@ -507,18 +510,48 @@ fn base_source_entries(
)); ));
} }
if let Some(content) = project_context.instructions.as_deref() { // #6526: assemble once, so the source row counts the content the prompt
// Same helper as ProjectContext::as_system_block, so the report's // keeps, and the budget/loss rows below carry no token cost of their own.
// source token derives from the same label the prompt shows. (The // Counting the raw text here and the retained text again there would
// entry below still displays the absolute source path for operators; // include discarded content and inflate the total even without a trim.
// only the prompt label is relativized.) let instruction_assembly = crate::project_context::active_instruction_budgets(
context_window_tokens.map(|tokens| tokens as usize),
Some(model),
)
.ok()
.map(|budgets| {
let mut instructions = crate::project_context::load_project_context_with_parents(workspace);
let configured: Vec<crate::prompts::InstructionSource> = configured_instruction_paths
.iter()
.cloned()
.map(Into::into)
.collect();
crate::project_context::assemble_instruction_block(&mut instructions, &configured, &budgets)
});
if let Some(block) = instruction_assembly
.as_ref()
.and_then(|assembly| assembly.block.as_deref())
{
builder.push(SourceEntry::text(
SourceKind::ProjectContext,
"Project instructions",
project_context
.source_path
.as_ref()
.map(|path| path.display().to_string()),
ActivationReason::FilePresent,
block,
CountingConfidence::High,
Some(5),
));
} else if let Some(content) = project_context.instructions.as_deref() {
let source = crate::project_context::project_instructions_source_label( let source = crate::project_context::project_instructions_source_label(
project_context.source_path.as_deref(), project_context.source_path.as_deref(),
); );
let mut block = format!( let mut block = format!(
"<project_instructions source=\"{source}\">\n{content}\n</project_instructions>" "<project_instructions source=\"{source}\">\n{content}\n</project_instructions>"
); );
// Include rules in the report when present
if let Some(rules) = &project_context.rules_block { if let Some(rules) = &project_context.rules_block {
block.push('\n'); block.push('\n');
block.push_str(rules); block.push_str(rules);
@@ -536,7 +569,6 @@ fn base_source_entries(
Some(5), Some(5),
)); ));
} else if let Some(rules) = &project_context.rules_block { } else if let Some(rules) = &project_context.rules_block {
// Rules exist without main instructions
builder.push(SourceEntry::text( builder.push(SourceEntry::text(
SourceKind::ProjectContext, SourceKind::ProjectContext,
"Project rules", "Project rules",
@@ -557,6 +589,39 @@ fn base_source_entries(
"no project context block available", "no project context block available",
)); ));
} }
if let Some(assembly) = instruction_assembly.as_ref() {
builder.push(SourceEntry::diagnostic(
SourceKind::ProjectContext,
"Instruction budget",
None::<String>,
ActivationReason::ConfigEnabled,
format!(
"global {} / {} estimated tokens; project {} / {} estimated tokens; policy {}; usable window {} tokens; static-prefix guard {} tokens",
assembly.global_tokens,
assembly.budgets.global_tokens,
assembly.project_tokens,
assembly.budgets.project_tokens,
assembly.budgets.policy.as_str(),
assembly.budgets.usable_window_tokens,
assembly.budgets.static_guard_tokens,
),
0,
Some(5),
));
for notice in assembly.notices.iter().filter(|notice| notice.is_loss()) {
builder.push(SourceEntry::diagnostic(
SourceKind::ProjectContextWarning,
format!("Instruction source: {}", notice.label()),
notice.path.as_ref().map(|path| path.display().to_string()),
ActivationReason::RuntimeState,
notice.to_string(),
0,
Some(5),
));
}
}
if !project_context.warnings.is_empty() { if !project_context.warnings.is_empty() {
let warnings = project_context.warnings.join("\n"); let warnings = project_context.warnings.join("\n");
let estimated_tokens = estimate_text_tokens_conservative(&warnings); let estimated_tokens = estimate_text_tokens_conservative(&warnings);
@@ -1017,6 +1082,40 @@ mod tests {
assert!(json.contains("branch_policy appears stale")); assert!(json.contains("branch_policy appears stale"));
} }
#[test]
fn context_report_shows_actions_and_budgets() {
let tmp = tempdir().expect("workspace");
std::fs::write(tmp.path().join("AGENTS.md"), "T".repeat(20_000)).expect("write agents");
crate::project_context::with_test_instruction_budget_config(
crate::project_context::InstructionBudgetConfig {
project_budget_tokens: Some(50),
..Default::default()
},
|| {
let report = build_headless_context_report(&Config::default(), tmp.path());
assert!(
report
.entries
.iter()
.any(|entry| entry.label == "Instruction budget"),
"the resolved per-scope budgets are a source-map row: {:?}",
report.entries
);
assert!(
report.entries.iter().any(|entry| {
entry.label.starts_with("Instruction source:")
&& entry
.truncation_reason
.as_deref()
.is_some_and(|reason| reason.contains("scope_budget"))
}),
"each loss names its action and limit kind: {:?}",
report.entries
);
},
);
}
#[test] #[test]
fn headless_context_report_uses_kimi_code_k3_route_context() { fn headless_context_report_uses_kimi_code_k3_route_context() {
let tmp = tempdir().expect("workspace"); let tmp = tempdir().expect("workspace");
+233 -37
View File
@@ -1017,6 +1017,16 @@ pub struct Engine {
codewhale_client_error: Option<String>, codewhale_client_error: Option<String>,
api_key_env_only_recovery: Option<String>, api_key_env_only_recovery: Option<String>,
session: Session, session: Session,
/// One-shot instruction-budget notice for the model (#6526), delivered as
/// a synthetic user message on the first turn and then cleared.
pending_instruction_notice: Option<String>,
/// Whether the static-prefix guard has already run for this session. The
/// guard is a safety net with a one-shot notice, not a per-turn meter.
static_prefix_guard_checked: bool,
/// The ceiling the guard settled on, kept so every later composition of
/// this session honours it: without it a plain turn would recompose with
/// the configured budget and put the trimmed files back.
active_guard_trim: Option<crate::project_context::InstructionGuardTrim>,
/// One lazy, session-scoped working kernel for inline `repl` blocks. /// One lazy, session-scoped working kernel for inline `repl` blocks.
/// Its context is refreshed before each run, while user-created Python /// Its context is refreshed before each run, while user-created Python
/// state stays alive across model turns. /// state stays alive across model turns.
@@ -2260,12 +2270,15 @@ impl Engine {
} else { } else {
prompts::PromptHost::Headless prompts::PromptHost::Headless
}; };
let system_prompt = if let Some(host) = host.as_ref() { let (system_prompt, instruction_assembly) = if let Some(host) = host.as_ref() {
host.system_prompt().clone() (host.system_prompt().clone(), None)
} else if api_config.runtime_chat_isolated { } else if api_config.runtime_chat_isolated {
SystemPrompt::Text(ISOLATED_CHAT_SYSTEM_PROMPT.to_string()) (
SystemPrompt::Text(ISOLATED_CHAT_SYSTEM_PROMPT.to_string()),
None,
)
} else { } else {
prompts::system_prompt_for_mode_with_context_skills_session_and_approval_for_host( let (prompt, assembly) = prompts::system_prompt_with_instruction_assembly_for_host(
&config.workspace, &config.workspace,
None, None,
Some(&config.skills_dir), Some(&config.skills_dir),
@@ -2293,8 +2306,13 @@ impl Engine {
mode: AppMode::Agent, mode: AppMode::Agent,
}, },
prompt_host, prompt_host,
) None,
);
(prompt, Some(assembly))
}; };
let pending_instruction_notice = instruction_assembly
.as_ref()
.and_then(|assembly| crate::project_context::render_notice_body(&assembly.notices));
let stable_prompt = Some(system_prompt); let stable_prompt = Some(system_prompt);
session.last_system_prompt_hash = Some(system_prompt_hash(stable_prompt.as_ref())); session.last_system_prompt_hash = Some(system_prompt_hash(stable_prompt.as_ref()));
session.system_prompt = stable_prompt; session.system_prompt = stable_prompt;
@@ -2472,6 +2490,9 @@ impl Engine {
codewhale_client_error, codewhale_client_error,
api_key_env_only_recovery, api_key_env_only_recovery,
session, session,
pending_instruction_notice,
static_prefix_guard_checked: false,
active_guard_trim: None,
repl_kernel: None, repl_kernel: None,
subagent_manager, subagent_manager,
subagent_execution_limits, subagent_execution_limits,
@@ -4193,6 +4214,11 @@ impl Engine {
workspace, workspace,
mode, mode,
} => { } => {
// A fresh conversation arrives with its own
// sources: re-arm the guard and rebuild the notice
// from the assembly that installs its header.
self.static_prefix_guard_checked = false;
self.active_guard_trim = None;
let plugin_workspace_changed = let plugin_workspace_changed =
self.plugin_registry.workspace() != workspace.as_path(); self.plugin_registry.workspace() != workspace.as_path();
let previous_session_id = self.session.id.clone(); let previous_session_id = self.session.id.clone();
@@ -9122,7 +9148,17 @@ impl Engine {
reason: &str, reason: &str,
) { ) {
self.record_project_trust_warning(); self.record_project_trust_warning();
let stable_prompt = self.compose_stable_system_prompt(context); let (stable_prompt, assembly) =
self.compose_stable_system_prompt_with_assembly(context, self.active_guard_trim);
// The assembly that installs the header is what the operator and the
// model must hear about: a route change can drop files that the
// construction-time assembly did not.
if let Some(body) = assembly
.as_ref()
.and_then(|assembly| crate::project_context::render_notice_body(&assembly.notices))
{
self.pending_instruction_notice = Some(body);
}
let stable_hash = system_prompt_hash(stable_prompt.as_ref()); let stable_hash = system_prompt_hash(stable_prompt.as_ref());
if self.session.system_prompt_override { if self.session.system_prompt_override {
@@ -9208,17 +9244,38 @@ impl Engine {
&self, &self,
context: &NextTurnPromptContext, context: &NextTurnPromptContext,
) -> Option<SystemPrompt> { ) -> Option<SystemPrompt> {
self.compose_stable_system_prompt_with_assembly(context, self.active_guard_trim)
.0
}
/// Compose the stable system prompt together with the instruction assembly
/// that produced it, optionally under a static-prefix guard trim.
///
/// The assembly carries the budgets and every trim/skip notice, so the
/// guard can trim the largest scope and the caller can tell the user and
/// the model what was lost.
pub(super) fn compose_stable_system_prompt_with_assembly(
&self,
context: &NextTurnPromptContext,
guard_trim: Option<crate::project_context::InstructionGuardTrim>,
) -> (
Option<SystemPrompt>,
Option<crate::project_context::InstructionAssembly>,
) {
if let Some(rlm) = self.rlm_host.as_ref() { if let Some(rlm) = self.rlm_host.as_ref() {
return Some(rlm.system_prompt.clone()); return (Some(rlm.system_prompt.clone()), None);
} }
if let Some(child) = self.child_host.as_ref() { if let Some(child) = self.child_host.as_ref() {
// The configured role is input to this same composer, and is // The configured role is input to this same composer, and is
// retained across route/mode/header refreshes. Volatile Native // retained across route/mode/header refreshes. Volatile Native
// contributions and work facts still append to Core Session. // contributions and work facts still append to Core Session.
return Some(child.system_prompt.clone()); return (Some(child.system_prompt.clone()), None);
} }
if self.api_config.runtime_chat_isolated { if self.api_config.runtime_chat_isolated {
return Some(SystemPrompt::Text(ISOLATED_CHAT_SYSTEM_PROMPT.to_string())); return (
Some(SystemPrompt::Text(ISOLATED_CHAT_SYSTEM_PROMPT.to_string())),
None,
);
} }
let user_memory_block = codewhale_runtime::native_memory::native_prompt_block_traced( let user_memory_block = codewhale_runtime::native_memory::native_prompt_block_traced(
self.config.memory_enabled, self.config.memory_enabled,
@@ -9237,35 +9294,174 @@ impl Engine {
&self.config.workspace, &self.config.workspace,
Some(self.session.id.as_str()), Some(self.session.id.as_str()),
); );
let base = let (base, assembly) = prompts::system_prompt_with_instruction_assembly_for_host(
prompts::system_prompt_for_mode_with_context_skills_session_and_approval_for_host( &self.config.workspace,
&self.config.workspace, None,
None, Some(&self.config.skills_dir),
Some(&self.config.skills_dir), Some(&self.config.instructions),
Some(&self.config.instructions), prompts::PromptSessionContext {
prompts::PromptSessionContext { user_memory_block: user_memory_block.as_deref(),
user_memory_block: user_memory_block.as_deref(), goal_objective: context.goal_objective.as_deref(),
goal_objective: context.goal_objective.as_deref(), project_context_pack_enabled: self.config.project_context_pack_enabled,
project_context_pack_enabled: self.config.project_context_pack_enabled, locale_tag: &self.config.locale_tag,
locale_tag: &self.config.locale_tag, translation_enabled: context.translation_enabled,
translation_enabled: context.translation_enabled, model_id: &context.model,
model_id: &context.model, context_window_override: Some(crate::route_budget::route_context_window_tokens(
context_window_override: Some( context.provider,
crate::route_budget::route_context_window_tokens( &context.model,
context.provider, context.route_limits,
&context.model, )),
context.route_limits, verbosity: context.verbosity.as_deref(),
), recovery_hint: recovery_hint.as_deref(),
), skills_discovery_mode: self.config.skills_discovery_mode,
verbosity: context.verbosity.as_deref(), plugin_registry: Some(self.plugin_registry.as_ref()),
recovery_hint: recovery_hint.as_deref(), mode: context.mode,
skills_discovery_mode: self.config.skills_discovery_mode, },
plugin_registry: Some(self.plugin_registry.as_ref()), prompt_host,
mode: context.mode, guard_trim,
}, );
prompt_host, (Some(base), Some(assembly))
}
/// Deliver the one-shot instruction-budget notice and, once per session,
/// enforce the static-prefix guard (#6526).
///
/// The notice reaches the model as a synthetic user message with runtime
/// provenance — the same mechanism LSP diagnostics use — so it is part of
/// the logged transcript rather than an invisible prompt edit.
pub(super) async fn apply_instruction_budget_notices(
&mut self,
tools: &[codewhale_models::Tool],
) {
if let Some(body) = self.pending_instruction_notice.take() {
self.add_session_message(self.runtime_text_message_with_turn_metadata(
body,
crate::core::ops::UserInputProvenance::Runtime,
))
.await;
}
if self.static_prefix_guard_checked {
return;
}
if let Some(body) = self.enforce_static_prefix_guard(tools).await {
self.add_session_message(self.runtime_text_message_with_turn_metadata(
body,
crate::core::ops::UserInputProvenance::Runtime,
))
.await;
}
}
/// Keep the stable prefix — base prompt, instructions, skills index and
/// tool schemas — under `static_prefix_guard_percent` of the usable
/// window.
///
/// Trims the largest instruction scope by the overage, recomposes once,
/// re-measures once. No loop: when every scope is already zero and the
/// prefix is still over, the session continues under `policy = "trim"`
/// with an explicit `static_guard_unsatisfied` notice (`policy = "refuse"`
/// is enforced at session start, before the engine exists).
async fn enforce_static_prefix_guard(
&mut self,
tools: &[codewhale_models::Tool],
) -> Option<String> {
if self.session.system_prompt_override {
return None;
}
let context = self.session.pinned_prompt_context.clone()?;
let window = crate::route_budget::route_context_window_tokens(
self.api_provider,
&self.session.model,
self.active_route_limits,
) as usize;
let budgets = crate::project_context::active_instruction_budgets(
Some(window),
Some(&self.session.model),
)
.unwrap_or_else(|_| crate::project_context::InstructionBudgets::defaults_for(Some(window)));
let measured = prompts::measure_static_prefix(self.session.system_prompt.as_ref(), tools);
if measured <= budgets.static_guard_tokens {
return None;
}
let over = measured - budgets.static_guard_tokens;
// The assembly reads directories and configured files: run it on a
// blocking worker so a slow disk cannot stall the engine's runtime
// thread before a provider request.
let workspace = self.config.workspace.clone();
let configured = self.config.instructions.clone();
let assembly = tokio::task::spawn_blocking(move || {
crate::project_context::instruction_assembly_for(&workspace, &configured, &budgets)
})
.await
.ok()?;
let scope = assembly
.largest_scope
.unwrap_or(crate::project_context::InstructionScope::Global);
let retained = match scope {
crate::project_context::InstructionScope::Global => assembly.global_tokens,
crate::project_context::InstructionScope::Project => assembly.project_tokens,
};
// Nothing left to trim: the guard is unsatisfiable. Continue under
// `policy = "trim"` with an explicit terminal notice — `refuse` is
// enforced at session start, before the engine exists.
if retained <= over {
self.session.pending_prefix_change_reason =
Some("instruction-budget:static-guard-unsatisfiable".to_string());
self.active_guard_trim =
Some(crate::project_context::InstructionGuardTrim { scope, tokens: 0 });
self.static_prefix_guard_checked = true;
return Some(
crate::project_context::InstructionNotice::static_guard(scope, over, true)
.to_string(),
); );
Some(base) }
// Trim by the overage from what the scope actually kept, so the
// recomposed prefix really loses that many tokens.
let trim = crate::project_context::InstructionGuardTrim {
scope,
tokens: retained - over,
};
let (prompt, trimmed_assembly) =
self.compose_stable_system_prompt_with_assembly(&context, Some(trim));
let prompt = prompt?;
let after = prompts::measure_static_prefix(Some(&prompt), tools);
self.session.system_prompt = Some(prompt);
self.session.last_system_prompt_hash =
Some(system_prompt_hash(self.session.system_prompt.as_ref()));
self.session.pending_prefix_change_reason =
Some("instruction-budget:static-guard".to_string());
self.active_guard_trim = Some(trim);
self.static_prefix_guard_checked = true;
let retained_after = trimmed_assembly
.as_ref()
.map_or(0, |assembly| assembly.tokens_kept());
let unsatisfiable = matches!(
crate::project_context::static_guard_outcome(
after,
&budgets,
Some(scope),
retained_after
),
crate::project_context::StaticGuardOutcome::Unsatisfiable { .. }
);
let mut body =
crate::project_context::InstructionNotice::static_guard(scope, over, unsatisfiable)
.to_string();
// The guard's own rebuild produces per-source losses; publish them with
// the scope-level sentence so the model sees which files went.
if let Some(losses) = trimmed_assembly
.as_ref()
.and_then(|assembly| crate::project_context::render_notice_body(&assembly.notices))
{
body.push('\n');
body.push_str(&losses);
}
Some(body)
} }
fn installed_next_turn_prompt_context(&self) -> NextTurnPromptContext { fn installed_next_turn_prompt_context(&self) -> NextTurnPromptContext {
@@ -188,6 +188,12 @@ impl Engine {
&progress.active_tool_names, &progress.active_tool_names,
strict_tool_mode, strict_tool_mode,
); );
// #6526: the instruction-budget notice reaches the model once, as a
// logged synthetic message, and the static-prefix guard runs once with
// the real tool catalog — the only place where the prefix and the tool
// schemas are known together.
self.apply_instruction_budget_notices(active_tools.as_deref().unwrap_or_default())
.await;
match self match self
.run_auto_compaction( .run_auto_compaction(
client.as_ref(), client.as_ref(),
+45
View File
@@ -438,6 +438,13 @@ pub(crate) async fn run_exec_agent(
); );
let active_route_limits = let active_route_limits =
crate::route_budget::known_route_limits(validated_route.candidate.limits()); crate::route_budget::known_route_limits(validated_route.candidate.limits());
// The startup instruction notice budgets against the window this route will
// actually use; the 128k default exists for callers that have no route.
let instruction_budget_window = crate::route_budget::route_context_window_tokens(
effective_provider,
&effective_model,
active_route_limits,
) as usize;
let max_subagents = if max_subagents let max_subagents = if max_subagents
== config.max_subagents_for_provider( == config.max_subagents_for_provider(
&config &config
@@ -740,6 +747,44 @@ pub(crate) async fn run_exec_agent(
eprintln!("warning: {sandbox_posture} — shell commands will run unrestricted"); eprintln!("warning: {sandbox_posture} — shell commands will run unrestricted");
} }
// #6526: headless runs report standing-instruction losses on stderr, the
// same content the interactive start puts in a system cell. The check
// covers every source the engine budgets, including the
// `--append-system-prompt` entry, and runs on a blocking worker: it walks
// directories and reads files.
let mut configured_sources: Vec<crate::prompts::InstructionSource> = execution_config
.instructions_paths()
.into_iter()
.map(Into::into)
.collect();
if let Some(extra) = append_system_prompt.as_ref() {
configured_sources.push(crate::prompts::InstructionSource::Inline {
name: "cli:append-system-prompt".to_string(),
content: extra.clone(),
});
}
let notice_workspace = workspace.clone();
let notice_model = effective_model.clone();
let notice = tokio::task::spawn_blocking(move || {
crate::project_context::startup_instruction_notice(
&notice_workspace,
&configured_sources,
Some(instruction_budget_window),
Some(&notice_model),
)
})
.await;
match notice {
Ok(Ok(Some(notice))) => {
if output_format == ExecOutputFormat::Text && !json_output {
eprintln!("{notice}");
}
}
Ok(Ok(None)) => {}
Ok(Err(error)) => anyhow::bail!(error),
Err(error) => anyhow::bail!("instruction check did not complete: {error}"),
}
let resuming_session = resume_session.is_some(); let resuming_session = resume_session.is_some();
let mut loaded_session_id = None; let mut loaded_session_id = None;
if let Some(saved) = resume_session { if let Some(saved) = resume_session {
+61
View File
@@ -9651,6 +9651,7 @@ fn load_config_with_cli_preferences(
// exec, ACP, and the app-server passthrough all resolve config here — so // exec, ACP, and the app-server passthrough all resolve config here — so
// the loader never has to be handed the setting at each of its call sites. // the loader never has to be handed the setting at each of its call sites.
install_foreign_instruction_imports(&config); install_foreign_instruction_imports(&config);
install_instruction_budget(&config)?;
Ok(config) Ok(config)
} }
@@ -9751,6 +9752,21 @@ fn apply_selected_fleet_operator_for_launch(
/// Unrecognized names are reported rather than dropped: a typo in this key /// Unrecognized names are reported rather than dropped: a typo in this key
/// silently means "import nothing", which is exactly the failure mode a user /// silently means "import nothing", which is exactly the failure mode a user
/// would not notice. /// would not notice.
/// Validate and install `[instruction_budget]` before anything can load
/// project context.
///
/// A broken table fails the launch: a nonsensical budget applied silently is
/// worse than a clear error, and the table is self-contained enough to be
/// checked at load time.
fn install_instruction_budget(config: &Config) -> Result<()> {
let table = config.instruction_budget.clone().unwrap_or_default();
table
.validate()
.map_err(|error| anyhow::anyhow!("invalid [instruction_budget]: {error}"))?;
crate::project_context::set_instruction_budget_config(table);
Ok(())
}
fn install_foreign_instruction_imports(config: &Config) { fn install_foreign_instruction_imports(config: &Config) {
let (imports, unknown) = crate::project_context::ForeignInstructionImports::from_config( let (imports, unknown) = crate::project_context::ForeignInstructionImports::from_config(
&config.project_instruction_imports, &config.project_instruction_imports,
@@ -13315,6 +13331,16 @@ fn merge_project_config_with_approval_baseline(
(See #417.)" (See #417.)"
); );
} }
// #6526: the per-scope instruction budget is user authority for the same
// reason `instructions` is — a cloned repository must not be able to
// shrink the user's global budget or force `policy = "refuse"`.
if table.contains_key("instruction_budget") {
eprintln!(
"warning: project-scope `instruction_budget` is ignored — \
set instruction budgets in user config instead. \
(See #417.)"
);
}
Ok(()) Ok(())
} }
@@ -19213,6 +19239,41 @@ instructions = ["./AGENTS.md", "./extra.md"]
); );
} }
#[test]
fn project_overlay_cannot_set_global_budget() {
let tmp = workspace_with_project_config(
r#"
[instruction_budget]
global_budget_tokens = 1
policy = "refuse"
"#,
);
let mut config = Config {
instruction_budget: Some(crate::project_context::InstructionBudgetConfig {
global_budget_tokens: Some(100_000),
..Default::default()
}),
..Config::default()
};
merge_project_config(&mut config, tmp.path());
assert_eq!(
config
.instruction_budget
.as_ref()
.and_then(|budget| budget.global_budget_tokens),
Some(100_000),
"a cloned repository must not shrink the user's global budget"
);
assert_eq!(
config
.instruction_budget
.as_ref()
.and_then(|budget| budget.policy),
None,
"a cloned repository must not force policy = refuse"
);
}
#[test] #[test]
fn project_overlay_empty_instructions_array_preserves_user_list() { fn project_overlay_empty_instructions_array_preserves_user_list() {
let tmp = workspace_with_project_config( let tmp = workspace_with_project_config(
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,647 @@
//! Per-scope instruction budget, selection, and notice behaviour (#6526).
//!
//! One test per branch the design specifies: the `AGENTS.md`/`CLAUDE.md`
//! truth table, skip-as-absent, symlink dedupe, per-scope trims, rules
//! charging, configured entries, and the `refuse` policy.
use super::*;
use tempfile::tempdir;
fn write(path: &Path, body: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("mkdir");
}
fs::write(path, body).expect("write");
}
fn budgets(global: usize, project: usize) -> InstructionBudgets {
InstructionBudgets {
global_tokens: global,
project_tokens: project,
static_guard_tokens: 900_000,
file_max_bytes: 8 * 1024 * 1024,
policy: InstructionBudgetPolicy::Trim,
claude_fallback: true,
usable_window_tokens: 1_000_000,
}
}
fn load_isolated(workspace: &Path, home: &Path) -> ProjectContext {
load_project_context_with_parents_and_home(workspace, Some(home))
}
fn assemble(
workspace: &Path,
home: &Path,
configured: &[crate::prompts::InstructionSource],
budgets: &InstructionBudgets,
) -> (ProjectContext, InstructionAssembly) {
let mut ctx = load_isolated(workspace, home);
let assembly = assemble_instruction_block(&mut ctx, configured, budgets);
(ctx, assembly)
}
fn claude_imports() -> ForeignInstructionImports {
ForeignInstructionImports::from_config(&["claude".to_string()]).0
}
// ── Selection ──────────────────────────────────────────────────────────────
#[test]
fn agents_md_wins_over_claude_md() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), "AGENTS_WINS");
write(&ws.path().join("CLAUDE.md"), "CLAUDE_LOSES");
let ctx = load_isolated(ws.path(), home.path());
let text = ctx.instructions.unwrap_or_default();
assert!(text.contains("AGENTS_WINS"), "{text}");
assert!(
!text.contains("CLAUDE_LOSES"),
"AGENTS.md wins; CLAUDE.md is not co-read without an import:\n{text}"
);
}
#[test]
fn claude_md_is_default_fallback() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("CLAUDE.md"), "CLAUDE_FALLBACK_TEXT");
let ctx = load_isolated(ws.path(), home.path());
assert!(
ctx.instructions
.unwrap_or_default()
.contains("CLAUDE_FALLBACK_TEXT")
);
}
#[test]
fn claude_fallback_can_be_disabled() {
let config = InstructionBudgetConfig {
claude_fallback: Some(false),
..Default::default()
};
with_test_instruction_budget_config(config, || {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("CLAUDE.md"), "CLAUDE_OPTED_OUT");
let ctx = load_isolated(ws.path(), home.path());
assert!(
!ctx.instructions
.unwrap_or_default()
.contains("CLAUDE_OPTED_OUT"),
"claude_fallback = false is an explicit opt-out"
);
});
}
#[test]
fn claude_import_reads_when_agents_absent_and_fallback_disabled() {
let config = InstructionBudgetConfig {
claude_fallback: Some(false),
..Default::default()
};
with_test_instruction_budget_config(config, || {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("CLAUDE.md"), "CLAUDE_EXPLICIT_IMPORT");
// The import set is explicit here: the process-wide default is
// "import nothing", and the fallback flag is off, so only the explicit
// `claude` import can read the file.
let mut ctx = load_project_context_with_parents_and_home_imports(
ws.path(),
Some(home.path()),
&claude_imports(),
);
let assembly = assemble_instruction_block(&mut ctx, &[], &budgets(1_000, 1_000));
assert!(
assembly
.block
.unwrap_or_default()
.contains("CLAUDE_EXPLICIT_IMPORT")
);
});
// The explicit import outranks the fallback flag: load through the import
// set the loader reads.
let ws = tempdir().unwrap();
write(&ws.path().join("CLAUDE.md"), "CLAUDE_IMPORTED");
let ctx = load_project_context_with_imports(ws.path(), &claude_imports());
assert!(
ctx.instructions
.unwrap_or_default()
.contains("CLAUDE_IMPORTED")
);
}
#[test]
fn explicit_claude_import_reads_both() {
let ws = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), "AGENTS_PRIMARY");
write(&ws.path().join("CLAUDE.md"), "CLAUDE_CO_READ");
let ctx = load_project_context_with_imports(ws.path(), &claude_imports());
let text = ctx.instructions.unwrap_or_default();
assert!(text.contains("AGENTS_PRIMARY"), "{text}");
assert!(
text.contains("CLAUDE_CO_READ"),
"an explicit claude import co-reads CLAUDE.md next to AGENTS.md:\n{text}"
);
let agents_at = text.find("AGENTS_PRIMARY").unwrap();
let claude_at = text.find("CLAUDE_CO_READ").unwrap();
assert!(agents_at < claude_at, "AGENTS.md reads first");
}
#[test]
fn agents_md_skipped_falls_back_to_claude() {
let config = InstructionBudgetConfig {
file_max_bytes: Some(16),
..Default::default()
};
with_test_instruction_budget_config(config, || {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), "A".repeat(200).as_str());
// Comfortably under the 16-byte ceiling above, or the fallback would
// be skipped for the same reason as `AGENTS.md`.
write(&ws.path().join("CLAUDE.md"), "CLAUDE_OK");
let ctx = load_isolated(ws.path(), home.path());
let text = ctx.instructions.unwrap_or_default();
assert!(
text.contains("CLAUDE_OK"),
"a skipped AGENTS.md counts as absent:\n{text}"
);
let skipped = ctx
.notices
.iter()
.find(|notice| notice.limit == InstructionLimit::FileCeiling)
.expect("the skip is reported");
assert_eq!(skipped.action, InstructionAction::Skipped);
assert!(
skipped
.fallback_selected
.as_deref()
.is_some_and(|label| label.contains("CLAUDE.md")),
"the notice names the file that replaced it: {skipped:?}"
);
});
}
#[test]
fn global_claude_is_last_resort_after_instructions_md() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(
&home.path().join(".codewhale/instructions.md"),
"GLOBAL_INSTRUCTIONS_WIN",
);
write(&home.path().join(".claude/CLAUDE.md"), "GLOBAL_CLAUDE_LAST");
let ctx = load_isolated(ws.path(), home.path());
let text = ctx.instructions.unwrap_or_default();
assert!(text.contains("GLOBAL_INSTRUCTIONS_WIN"), "{text}");
assert!(
!text.contains("GLOBAL_CLAUDE_LAST"),
"a foreign file never outranks codewhale's own:\n{text}"
);
}
#[test]
fn global_claude_is_read_when_nothing_else_exists() {
let config = InstructionBudgetConfig {
claude_fallback: Some(true),
..Default::default()
};
with_test_instruction_budget_config(config, || {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&home.path().join(".claude/CLAUDE.md"), "GLOBAL_CLAUDE_ONLY");
let ctx = load_isolated(ws.path(), home.path());
assert!(
ctx.instructions
.unwrap_or_default()
.contains("GLOBAL_CLAUDE_ONLY")
);
});
}
#[cfg(unix)]
#[test]
fn symlinked_duplicate_is_loaded_once() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
let global = home.path().join(".codewhale/AGENTS.md");
write(&global, "SHARED_SYMLINK_BODY");
std::os::unix::fs::symlink(&global, ws.path().join("AGENTS.md")).expect("symlink");
let ctx = load_isolated(ws.path(), home.path());
let text = ctx.instructions.unwrap_or_default();
assert_eq!(
text.matches("SHARED_SYMLINK_BODY").count(),
1,
"one physical file loads once:\n{text}"
);
assert!(
ctx.notices.iter().any(|notice| {
notice.path.as_deref() == Some(ws.path().join("AGENTS.md").as_path())
&& notice.action == InstructionAction::Skipped
}),
"the duplicate is reported, not silently dropped: {:?}",
ctx.notices
);
}
// ── Per-scope budgets ──────────────────────────────────────────────────────
#[test]
fn no_trim_within_scope_budget() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), "SMALL PROJECT FILE");
let (ctx, assembly) = assemble(ws.path(), home.path(), &[], &budgets(10_000, 10_000));
assert!(
!assembly.notices.iter().any(|notice| notice.is_loss()),
"nothing is lost while a source is within its budget: {:?}",
assembly.notices
);
assert_eq!(
ctx.instructions.as_deref(),
Some("SMALL PROJECT FILE"),
"content is unmodified"
);
}
#[test]
fn per_scope_budgets_are_independent() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(
&home.path().join(".codewhale/AGENTS.md"),
format!("GLOBAL {}", "g".repeat(4_000)).as_str(),
);
write(&ws.path().join("AGENTS.md"), "PROJECT INTACT");
let (_ctx, assembly) = assemble(ws.path(), home.path(), &[], &budgets(50, 10_000));
let global_loss = assembly
.notices
.iter()
.find(|notice| notice.is_loss())
.expect("the global scope is over its own budget");
assert_eq!(global_loss.scope, InstructionScope::Global);
assert!(
!assembly
.notices
.iter()
.any(|notice| notice.is_loss() && notice.scope == InstructionScope::Project),
"a huge global file does not shrink the project scope: {:?}",
assembly.notices
);
assert!(
assembly
.block
.unwrap_or_default()
.contains("PROJECT INTACT")
);
}
#[test]
fn trim_keeps_the_nearest_scope() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
fs::create_dir_all(ws.path().join(".git")).unwrap();
write(&ws.path().join(".git/HEAD"), "ref: refs/heads/main\n");
write(
&ws.path().join("AGENTS.md"),
format!("ROOT-MARKER {}", "R".repeat(20_000)).as_str(),
);
let nested = ws.path().join("crates").join("tui");
write(&nested.join("AGENTS.md"), "NEAREST-MARKER stays");
let (_ctx, assembly) = assemble(&nested, home.path(), &[], &budgets(10_000, 200));
let block = assembly.block.unwrap_or_default();
assert!(block.contains("NEAREST-MARKER stays"), "{block}");
assert!(
!block.contains("ROOT-MARKER"),
"the broadest segment is dropped first:\n{block}"
);
}
#[test]
fn single_oversized_file_keeps_the_head() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
let body = format!("HEAD-MARKER {}", "z".repeat(20_000));
write(&ws.path().join("AGENTS.md"), &body);
let (_ctx, assembly) = assemble(ws.path(), home.path(), &[], &budgets(10_000, 300));
let block = assembly.block.unwrap_or_default();
assert!(block.contains("HEAD-MARKER"), "{block}");
let head_at = block.find("HEAD-MARKER").expect("head marker");
assert!(
head_at < block.find("instructions trimmed").unwrap_or(usize::MAX),
"the head survives and the marker records the cut:\n{block}"
);
assert!(block.contains("instructions trimmed"), "{block}");
assert!(
block.contains("estimated tokens"),
"the marker carries numbers"
);
assert!(assembly.global_tokens <= 300);
}
#[test]
fn zero_bytes_left_is_reported_as_skipped() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), &"q".repeat(4_000));
let (_ctx, assembly) = assemble(ws.path(), home.path(), &[], &budgets(10_000, 1));
let notice = assembly
.notices
.iter()
.find(|notice| notice.scope == InstructionScope::Project)
.expect("a notice");
assert_eq!(
notice.action,
InstructionAction::Skipped,
"no source bytes left means skipped, not trimmed: {notice:?}"
);
assert_eq!(notice.limit, InstructionLimit::ScopeBudget);
}
#[test]
fn rules_share_the_project_scope_budget() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), &"I".repeat(1_000));
write(&ws.path().join(".codewhale/rules/a.md"), &"R".repeat(4_000));
let tokens_for_instructions = estimate_instruction_tokens(&"I".repeat(1_000));
let (_ctx, assembly) = assemble(
ws.path(),
home.path(),
&[],
&budgets(10_000, tokens_for_instructions + 10),
);
let rules = assembly
.notices
.iter()
.find(|notice| notice.origin == InstructionOrigin::Rules)
.expect("rules are charged after the chain");
assert_eq!(rules.limit, InstructionLimit::RulesBudget);
}
#[test]
fn rules_block_is_head_trimmed_with_numbers() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), "SHORT");
write(&ws.path().join(".codewhale/rules/a.md"), &"R".repeat(8_000));
let (_ctx, assembly) = assemble(ws.path(), home.path(), &[], &budgets(10_000, 400));
let block = assembly.block.unwrap_or_default();
assert!(block.contains("rules block trimmed"), "{block}");
assert!(
block.contains("estimated tokens"),
"numbers, not just a marker"
);
}
// ── Per-file ceiling, empties, cap, duplicates ─────────────────────────────
#[test]
fn file_over_ceiling_is_skipped_not_trimmed() {
let config = InstructionBudgetConfig {
file_max_bytes: Some(8),
..Default::default()
};
with_test_instruction_budget_config(config, || {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(
&ws.path().join("AGENTS.md"),
"THIS IS LONGER THAN EIGHT BYTES",
);
let ctx = load_isolated(ws.path(), home.path());
assert!(
!ctx.instructions
.unwrap_or_default()
.contains("LONGER THAN EIGHT")
);
let notice = ctx
.notices
.iter()
.find(|notice| notice.limit == InstructionLimit::FileCeiling)
.expect("the ceiling is reported");
assert_eq!(notice.action, InstructionAction::Skipped);
});
}
#[test]
fn empty_file_is_skipped() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), " \n\t\n");
write(&ws.path().join("CLAUDE.md"), "CLAUDE_AFTER_EMPTY");
let ctx = load_isolated(ws.path(), home.path());
assert!(
ctx.notices
.iter()
.any(|notice| notice.limit == InstructionLimit::EmptyFile)
);
assert!(
ctx.instructions
.unwrap_or_default()
.contains("CLAUDE_AFTER_EMPTY")
);
}
#[test]
fn rules_cap_is_reported() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
for index in 0..MAX_RULES_FILES + 5 {
write(
&ws.path()
.join(format!(".codewhale/rules/rule_{index:03}.md")),
"rule body",
);
}
let ctx = load_isolated(ws.path(), home.path());
let capped = ctx
.notices
.iter()
.filter(|notice| notice.limit == InstructionLimit::RulesCap)
.count();
assert_eq!(capped, 5, "every file beyond the cap is reported");
}
// ── Configured entries and the cache ───────────────────────────────────────
#[test]
fn configured_instructions_are_read_fresh() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
let extra = ws.path().join("extra.md");
write(&extra, "VERSION_ONE");
let configured = vec![crate::prompts::InstructionSource::File(extra.clone())];
let (_ctx, first) = assemble(
ws.path(),
home.path(),
&configured,
&budgets(10_000, 10_000),
);
assert!(first.block.unwrap_or_default().contains("VERSION_ONE"));
write(&extra, "VERSION_TWO");
let (_ctx, second) = assemble(
ws.path(),
home.path(),
&configured,
&budgets(10_000, 10_000),
);
let block = second.block.unwrap_or_default();
assert!(
block.contains("VERSION_TWO") && !block.contains("VERSION_ONE"),
"configured entries are read after the cache lookup:\n{block}"
);
}
#[test]
fn inline_instructions_stay_outside_cache() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), "PROJECT");
let configured = vec![crate::prompts::InstructionSource::Inline {
name: "embedded:test".to_string(),
content: "INLINE_ONLY".to_string(),
}];
let (_ctx, assembly) = assemble(
ws.path(),
home.path(),
&configured,
&budgets(10_000, 10_000),
);
assert!(assembly.block.unwrap_or_default().contains("INLINE_ONLY"));
let cached = load_isolated(ws.path(), home.path());
assert!(
!cached
.instructions
.unwrap_or_default()
.contains("INLINE_ONLY"),
"inline content never enters the file-keyed cache"
);
}
#[test]
fn instructions_array_counts_toward_global() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), "PROJECT_KEPT");
let global_file = ws.path().join("global-extra.md");
write(&global_file, &"G".repeat(4_000));
let configured = vec![crate::prompts::InstructionSource::File(global_file)];
let (_ctx, assembly) = assemble(ws.path(), home.path(), &configured, &budgets(60, 10_000));
assert!(
assembly
.notices
.iter()
.any(|notice| notice.is_loss() && notice.scope == InstructionScope::Global),
"configured entries are global authority: {:?}",
assembly.notices
);
assert!(assembly.block.unwrap_or_default().contains("PROJECT_KEPT"));
assert!(assembly.project_tokens > 0);
}
#[test]
fn cache_returns_untrimmed_segments_across_budgets() {
let ws = tempdir().unwrap();
let home = tempdir().unwrap();
let body = format!("FULL_BODY {}", "c".repeat(10_000));
write(&ws.path().join("AGENTS.md"), &body);
let (_small, _assembly) = assemble(ws.path(), home.path(), &[], &budgets(10_000, 50));
let cached = load_isolated(ws.path(), home.path());
assert_eq!(
cached.instructions.as_deref().map(str::len),
Some(body.len()),
"a trim must not be written back into the cache"
);
assert_eq!(cached.instruction_segments.len(), 1);
}
// ── Policy ────────────────────────────────────────────────────────────────
#[test]
fn refuse_policy_stops_on_trim_but_not_on_skip() {
let trim_config = InstructionBudgetConfig {
project_budget_tokens: Some(10),
policy: Some(InstructionBudgetPolicy::Refuse),
..Default::default()
};
with_test_instruction_budget_config(trim_config, || {
let ws = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), &"T".repeat(4_000));
let outcome = startup_instruction_notice(ws.path(), &[], Some(128_000), None);
let error = outcome.expect_err("refuse stops on a trim");
assert!(error.contains("refuse"), "{error}");
});
let skip_config = InstructionBudgetConfig {
file_max_bytes: Some(8),
policy: Some(InstructionBudgetPolicy::Refuse),
..Default::default()
};
with_test_instruction_budget_config(skip_config, || {
let ws = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), &"S".repeat(1_000));
let notice = startup_instruction_notice(ws.path(), &[], Some(128_000), None)
.expect("a skip is not a refusal")
.expect("a skip is reported");
assert!(notice.contains("skipped"), "{notice}");
});
}
#[test]
fn startup_notice_names_the_loss_and_numbers() {
with_test_instruction_budget_config(
InstructionBudgetConfig {
project_budget_tokens: Some(50),
..Default::default()
},
|| {
let ws = tempdir().unwrap();
write(&ws.path().join("AGENTS.md"), &"N".repeat(4_000));
let notice = startup_instruction_notice(ws.path(), &[], Some(128_000), None)
.expect("valid config")
.expect("a trim is reported");
assert!(notice.contains("trimmed"), "{notice}");
assert!(notice.contains("estimated tokens"), "{notice}");
assert!(notice.contains("bytes"), "{notice}");
},
);
}
#[test]
fn file_max_bytes_zero_is_rejected() {
let config = InstructionBudgetConfig {
file_max_bytes: Some(0),
..Default::default()
};
assert!(config.validate().is_err());
}
+41 -1
View File
@@ -6,6 +6,8 @@ use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
use super::budget::{InstructionNotice, InstructionSegment};
// === Errors === // === Errors ===
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -30,11 +32,38 @@ pub(crate) enum ProjectContextError {
Empty { path: PathBuf }, Empty { path: PathBuf },
} }
impl ProjectContextError {
/// Machine-readable reason, so a skip can name the limit that produced it.
pub(crate) fn kind(&self) -> super::budget::InstructionLimit {
match self {
Self::TooLarge { .. } => super::budget::InstructionLimit::FileCeiling,
Self::Empty { .. } => super::budget::InstructionLimit::EmptyFile,
Self::Metadata { .. } | Self::Read { .. } => super::budget::InstructionLimit::None,
}
}
}
/// Result of loading project context /// Result of loading project context
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ProjectContext { pub struct ProjectContext {
/// The loaded instructions content /// The loaded instructions content.
///
/// This is the rendered join of [`Self::instruction_segments`] — global
/// scope first, then the project chain — and is rebuilt after every
/// budget trim, so a consumer that only reads the joined string always
/// sees post-budget content.
pub instructions: Option<String>, pub instructions: Option<String>,
/// Raw instruction content kept per source, untrimmed.
///
/// Segment-level trimming needs to know where one file ends and the next
/// begins; a flattened string cannot answer that. The cached project
/// context is therefore stored raw and trimmed per request, so a context
/// window change never invalidates the file-keyed cache and a content
/// change never reuses trimmed text.
pub instruction_segments: Vec<InstructionSegment>,
/// Every instruction source that was trimmed, skipped, or refused while
/// loading or budgeting, for the startup notice and `/context report`.
pub notices: Vec<InstructionNotice>,
/// Auto-discovered rules from `.codewhale/rules/` / `.claude/rules/`. /// Auto-discovered rules from `.codewhale/rules/` / `.claude/rules/`.
/// Kept separate from `instructions` so rules alone don't block /// Kept separate from `instructions` so rules alone don't block
/// parent-directory AGENTS.md discovery via `has_instructions()`. /// parent-directory AGENTS.md discovery via `has_instructions()`.
@@ -61,6 +90,8 @@ impl ProjectContext {
pub fn empty(project_root: PathBuf) -> Self { pub fn empty(project_root: PathBuf) -> Self {
Self { Self {
instructions: None, instructions: None,
instruction_segments: Vec::new(),
notices: Vec::new(),
rules_block: None, rules_block: None,
source_path: None, source_path: None,
warnings: Vec::new(), warnings: Vec::new(),
@@ -76,6 +107,15 @@ impl ProjectContext {
self.instructions.is_some() self.instructions.is_some()
} }
/// Rebuild [`Self::instructions`] from the current segments.
///
/// Called after assembly and after every trim. Global scope renders
/// first so the workspace keeps the last word, exactly as the single
/// merged block did before segments existed.
pub(crate) fn rebuild_joined_instructions(&mut self) {
self.instructions = super::render_joined_instructions(&self.instruction_segments);
}
/// Get the instructions as a formatted block for system prompt. /// Get the instructions as a formatted block for system prompt.
/// ///
/// The Codewhale repo constitution (`.codewhale/constitution.json`), when /// The Codewhale repo constitution (`.codewhale/constitution.json`), when
+198 -178
View File
@@ -12,7 +12,7 @@
use crate::project_context::load_project_context_with_parents; use crate::project_context::load_project_context_with_parents;
use codewhale_config::AppMode; use codewhale_config::AppMode;
use codewhale_models::{SystemBlock, SystemPrompt}; use codewhale_models::{SystemBlock, SystemPrompt, Tool};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex}; use std::sync::{LazyLock, Mutex};
@@ -105,12 +105,13 @@ pub(crate) fn resolve_handoff_path(workspace: &Path) -> (PathBuf, &'static str)
} }
} }
/// Per-file size cap for `instructions = [...]` entries (#454). Mirrors /// Per-file size cap for the workspace relay artifact (`/relay`, compaction).
/// the existing project-context cap in `project_context::load_context_file` ///
/// so a malicious / oversized include can't blow the prompt budget on /// Deliberately separate from the instruction-file ceiling: the relay is a
/// its own. Files larger than this are truncated with an explicit `[…truncated: N bytes omitted]` /// workspace-writable checkpoint with its own trim marker, and
/// marker rather than skipped entirely so the model still sees the head. /// `[instruction_budget] policy = "refuse"` must not change how the relay is
const INSTRUCTIONS_FILE_MAX_BYTES: usize = 100 * 1024; /// read.
const RELAY_ARTIFACT_MAX_BYTES: usize = 100 * 1024;
fn read_prompt_file_bounded(path: &Path, cap: usize) -> std::io::Result<(String, usize)> { fn read_prompt_file_bounded(path: &Path, cap: usize) -> std::io::Result<(String, usize)> {
read_prompt_text_bounded(std::fs::File::open(path)?, cap) read_prompt_text_bounded(std::fs::File::open(path)?, cap)
@@ -439,10 +440,9 @@ impl Drop for RecordedEnvironmentGuard {
/// keep working with a `.into()` upgrade at the call site. /// keep working with a `.into()` upgrade at the call site.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum InstructionSource { pub enum InstructionSource {
/// Load this file from disk at prompt-render time. Original behavior: /// Read this file from disk when the instruction assembly runs.
/// missing files are skipped with a warning, oversized files are /// Missing, empty, and over-`file_max_bytes` files are skipped with a
/// truncated to `INSTRUCTIONS_FILE_MAX_BYTES` with an `[…elided]` /// notice; a file that fits is charged to the global scope budget.
/// marker.
File(PathBuf), File(PathBuf),
/// Use the provided string directly. `name` becomes the /// Use the provided string directly. `name` becomes the
/// `<instructions source="…">` attribute (typically a synthetic /// `<instructions source="…">` attribute (typically a synthetic
@@ -462,63 +462,6 @@ impl From<&PathBuf> for InstructionSource {
} }
} }
/// Render the `instructions = [...]` config array as a single
/// system-prompt block (#454). Each source is processed in declared order;
/// missing `File` sources are skipped with a tracing warning so a stale entry
/// doesn't fail the launch. Empty input (or all sources missing/empty)
/// returns `None` so callers append nothing.
fn render_instructions_block(sources: &[InstructionSource]) -> Option<String> {
let mut sections: Vec<String> = Vec::new();
for source in sources {
let mut total_len: Option<usize> = None;
let (raw_source_name, raw_content): (String, String) = match source {
InstructionSource::File(path) => {
match read_prompt_file_bounded(path, INSTRUCTIONS_FILE_MAX_BYTES) {
Ok((raw, full_len)) => {
// `full_len` covers bytes the bounded read left unread.
total_len = Some(full_len);
(path.display().to_string(), raw)
}
Err(err) => {
tracing::warn!(
target: "instructions",
?err,
?path,
"skipping unreadable instructions file"
);
continue;
}
}
}
InstructionSource::Inline { name, content } => (name.clone(), content.clone()),
};
let trimmed = raw_content.trim();
if trimmed.is_empty() {
continue;
}
// A bounded read that stopped short makes the file's own length the
// total; otherwise the trimmed text is everything there was.
let total = match total_len {
Some(full_len) if raw_content.len() < full_len => full_len,
_ => trimmed.len(),
};
let body = cap_prompt_text(
trimmed,
total,
INSTRUCTIONS_FILE_MAX_BYTES,
"consider splitting this instructions file",
);
sections.push(format!(
"<instructions source=\"{raw_source_name}\">\n{body}\n</instructions>"
));
}
if sections.is_empty() {
None
} else {
Some(sections.join("\n\n"))
}
}
/// Read the workspace-local relay artifact, if present, and format it as a /// Read the workspace-local relay artifact, if present, and format it as a
/// system-prompt block. Returns `None` when the file is absent or empty so /// system-prompt block. Returns `None` when the file is absent or empty so
/// callers can keep the default-uncluttered prompt for fresh workspaces. /// callers can keep the default-uncluttered prompt for fresh workspaces.
@@ -526,7 +469,7 @@ fn load_handoff_block(workspace: &Path) -> Option<String> {
let (path, shown) = resolve_handoff_path(workspace); let (path, shown) = resolve_handoff_path(workspace);
// The relay is workspace-writable, so it gets the same per-file cap as // The relay is workspace-writable, so it gets the same per-file cap as
// an instructions file rather than an unbounded read into the prompt. // an instructions file rather than an unbounded read into the prompt.
let (raw, full_len) = read_prompt_file_bounded(&path, INSTRUCTIONS_FILE_MAX_BYTES).ok()?; let (raw, full_len) = read_prompt_file_bounded(&path, RELAY_ARTIFACT_MAX_BYTES).ok()?;
let trimmed = raw.trim(); let trimmed = raw.trim();
if trimmed.is_empty() { if trimmed.is_empty() {
return None; return None;
@@ -539,7 +482,7 @@ fn load_handoff_block(workspace: &Path) -> Option<String> {
let relay = cap_prompt_text( let relay = cap_prompt_text(
trimmed, trimmed,
total, total,
INSTRUCTIONS_FILE_MAX_BYTES, RELAY_ARTIFACT_MAX_BYTES,
"shorten the relay artifact", "shorten the relay artifact",
); );
Some(format!( Some(format!(
@@ -1284,6 +1227,33 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo
session_context: PromptSessionContext<'_>, session_context: PromptSessionContext<'_>,
prompt_host: PromptHost, prompt_host: PromptHost,
) -> SystemPrompt { ) -> SystemPrompt {
system_prompt_with_instruction_assembly_for_host(
workspace,
_working_set_summary,
skills_dir,
instructions,
session_context,
prompt_host,
None,
)
.0
}
/// Compose the system prompt **and** the instruction assembly that produced
/// its instruction block.
///
/// The assembly carries the per-scope budgets and every trim/skip notice, so
/// the engine can enforce the static-prefix guard and surface the losses to
/// the user, the model, and `/context` from one measurement.
pub(crate) fn system_prompt_with_instruction_assembly_for_host(
workspace: &Path,
_working_set_summary: Option<&str>,
skills_dir: Option<&Path>,
instructions: Option<&[InstructionSource]>,
session_context: PromptSessionContext<'_>,
prompt_host: PromptHost,
guard_trim: Option<crate::project_context::InstructionGuardTrim>,
) -> (SystemPrompt, crate::project_context::InstructionAssembly) {
// One base prompt for every host (AGENTS.md: `BASE_PROMPT` is the sole // One base prompt for every host (AGENTS.md: `BASE_PROMPT` is the sole
// base prompt). Headless still skips interactive ceremony layers below — // base prompt). Headless still skips interactive ceremony layers below —
// the execution profile and authority recap — which are host chrome, not // the execution profile and authority recap — which are host chrome, not
@@ -1299,8 +1269,34 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo
&default_layers, &default_layers,
); );
// Load project context from workspace // Load project context from workspace, then charge it against the
let project_context = load_project_context_with_parents(workspace); // per-scope budgets. The cache keeps raw segments; trimming happens here,
// per request, so a window change never invalidates the file cache and a
// content change never reuses trimmed text.
let mut project_context = load_project_context_with_parents(workspace);
let window_tokens = session_context
.context_window_override
.map(|window| usize::try_from(window).unwrap_or(usize::MAX));
let mut budgets = match crate::project_context::active_instruction_budgets(
window_tokens,
Some(session_context.model_id),
) {
Ok(budgets) => budgets,
Err(error) => {
// Validation runs when the table is installed, so reaching this
// branch means a budget was resolved before config was applied.
tracing::error!(target: "project_context", %error, "invalid instruction budget");
crate::project_context::InstructionBudgets::defaults_for(window_tokens)
}
};
if let Some(trim) = guard_trim {
budgets = budgets.with_scope_ceiling(trim.scope, trim.tokens);
}
let assembly = crate::project_context::assemble_instruction_block(
&mut project_context,
instructions.unwrap_or(&[]),
&budgets,
);
// 0. Locale-native reinforcement preamble (#1118 follow-up). When the // 0. Locale-native reinforcement preamble (#1118 follow-up). When the
// user's UI locale is non-English we prepend a short native-script // user's UI locale is non-English we prepend a short native-script
@@ -1317,7 +1313,7 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo
// `load_project_context_with_parents` generates an in-memory bounded // `load_project_context_with_parents` generates an in-memory bounded
// overview when no context file exists, so the fallback should usually be // overview when no context file exists, so the fallback should usually be
// available without writing project-local files. // available without writing project-local files.
let mut full_prompt = if let Some(project_block) = project_context.as_system_block() { let mut full_prompt = if let Some(project_block) = assembly.block.as_deref() {
format!("{}\n\n{project_block}", composed.trim()) format!("{}\n\n{project_block}", composed.trim())
} else { } else {
// Extremely unlikely: context generation failed (e.g. filesystem error). // Extremely unlikely: context generation failed (e.g. filesystem error).
@@ -1450,8 +1446,11 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo
} }
let workspace_body = workspace_parts.join("\n\n"); let workspace_body = workspace_parts.join("\n\n");
// Permissions fragment: configured `instructions = [...]` files (#454). // Configured `instructions = [...]` files (#454) are global-scope
let permissions_body = instructions.and_then(render_instructions_block); // instruction authority now: the assembly reads and charges them, and they
// render inside the project-instructions block instead of as a separate
// world-state fragment. Exactly one owner, one charge.
let permissions_body: Option<String> = None;
// Route fragment: verbosity / translation posture (the model id was // Route fragment: verbosity / translation posture (the model id was
// removed by the turn-meta diet — it is telemetry the model cannot act on). // removed by the turn-meta diet — it is telemetry the model cannot act on).
@@ -1505,7 +1504,37 @@ pub(crate) fn system_prompt_for_mode_with_context_skills_session_and_approval_fo
}); });
} }
SystemPrompt::Blocks(blocks) (SystemPrompt::Blocks(blocks), assembly)
}
/// Estimated tokens of the **stable** prefix: the constitution block — base
/// prompt, instructions, skills index, execution profile — plus the tool
/// schemas the request carries.
///
/// Volatile world-state blocks (environment, memory, goals, relay) are
/// excluded on purpose: they drift mid-session, are not part of the pinned
/// KV-cache prefix, and carry their own budgets. Only the first block plus the
/// tool catalog is what the static-prefix guard protects.
#[must_use]
pub(crate) fn measure_static_prefix(system: Option<&SystemPrompt>, tools: &[Tool]) -> usize {
let text_tokens = match system {
Some(SystemPrompt::Text(text)) => {
crate::compaction::estimate_text_tokens_conservative(text)
}
Some(SystemPrompt::Blocks(blocks)) => blocks.first().map_or(0, |block| {
crate::compaction::estimate_text_tokens_conservative(&block.text)
}),
None => 0,
};
let tool_tokens: usize = tools
.iter()
.map(|tool| {
serde_json::to_string(tool).map_or(0, |json| {
crate::compaction::estimate_text_tokens_conservative(&json)
})
})
.sum();
text_tokens.saturating_add(tool_tokens)
} }
/// Flatten a system prompt to joined text (tests + debug inspectors). /// Flatten a system prompt to joined text (tests + debug inspectors).
@@ -3773,12 +3802,6 @@ mod tests {
assert!(!prompt.contains("# Session relay")); assert!(!prompt.contains("# Session relay"));
} }
#[test]
fn render_instructions_block_returns_none_for_empty_input() {
let empty: &[super::InstructionSource] = &[];
assert!(super::render_instructions_block(empty).is_none());
}
/// #4632 — The system prompt prefix (the byte-stable part cached by /// #4632 — The system prompt prefix (the byte-stable part cached by
/// inference servers) must never contain private content: absolute /// inference servers) must never contain private content: absolute
/// filesystem paths, API keys, or home-directory references. /// filesystem paths, API keys, or home-directory references.
@@ -3832,112 +3855,109 @@ mod tests {
); );
} }
#[test] fn test_tool(name: &str, description: &str) -> Tool {
fn render_instructions_block_skips_missing_files_with_warning() { Tool {
let tmp = tempdir().expect("tempdir"); tool_type: Some("function".to_string()),
let real = tmp.path().join("real.md"); name: name.to_string(),
std::fs::write(&real, "real content here").unwrap(); description: description.to_string(),
let bogus = tmp.path().join("does-not-exist.md"); input_schema: serde_json::json!({
"type": "object",
let block = super::render_instructions_block(&[bogus.clone().into(), real.clone().into()]) "properties": {"path": {"type": "string"}}
.expect("present file should produce a block"); }),
assert!(block.contains("real content here")); allowed_callers: None,
assert!(block.contains(&real.display().to_string())); defer_loading: None,
// Bogus path is skipped, not rendered. input_examples: None,
assert!(!block.contains(&bogus.display().to_string())); strict: None,
cache_control: None,
}
} }
#[test] #[test]
fn render_instructions_block_concatenates_in_declared_order() { fn static_prefix_guard_measures_tool_schemas() {
let tmp = tempdir().expect("tempdir"); let prompt = SystemPrompt::Blocks(vec![SystemBlock {
let a = tmp.path().join("a.md"); block_type: "text".to_string(),
let b = tmp.path().join("b.md"); text: "constitution".to_string(),
std::fs::write(&a, "ALPHA_MARKER").unwrap(); cache_control: None,
std::fs::write(&b, "BRAVO_MARKER").unwrap(); }]);
let without_tools = super::measure_static_prefix(Some(&prompt), &[]);
let block = super::render_instructions_block(&[a.into(), b.into()]).expect("non-empty"); let with_tools = super::measure_static_prefix(
let alpha_pos = block.find("ALPHA_MARKER").expect("alpha rendered"); Some(&prompt),
let bravo_pos = block.find("BRAVO_MARKER").expect("bravo rendered"); &[test_tool("read_file", "Read a file from the workspace")],
);
assert!( assert!(
alpha_pos < bravo_pos, with_tools > without_tools,
"instructions must concatenate in declared order" "tool schemas are part of the stable prefix ({with_tools} vs {without_tools})"
); );
} }
#[test] #[test]
fn render_instructions_block_skips_empty_files() { fn stable_prefix_guard_ignores_memory_growth() {
let tmp = tempdir().expect("tempdir"); let stable = SystemBlock {
let empty = tmp.path().join("empty.md"); block_type: "text".to_string(),
let real = tmp.path().join("real.md"); text: "stable prefix".to_string(),
std::fs::write(&empty, " \n \n").unwrap(); cache_control: None,
std::fs::write(&real, "real content").unwrap();
let block =
super::render_instructions_block(&[empty.into(), real.into()]).expect("non-empty");
// Empty file produces no `<instructions>` section, only the real one.
let count = block.matches("<instructions").count();
assert_eq!(count, 1, "only the non-empty file should produce a section");
}
#[test]
fn render_instructions_block_truncates_oversize_files() {
let tmp = tempdir().expect("tempdir");
let big = tmp.path().join("big.md");
// 200 KiB of content — well above the 100 KiB cap.
std::fs::write(&big, "X".repeat(200 * 1024)).unwrap();
let block = super::render_instructions_block(&[big.into()]).expect("non-empty");
assert!(block.contains("[…truncated:"), "truncation marker missing");
// Block should be much smaller than the original file.
assert!(
block.len() < 110 * 1024,
"block should be capped near 100 KiB"
);
}
/// `InstructionSource::Inline` bypasses disk reads — the content is used
/// directly and `name` becomes the `<instructions source="…">` attribute.
/// Empty / oversize handling mirrors `File` variant.
#[test]
fn render_instructions_block_handles_inline_source() {
let block = super::render_instructions_block(&[super::InstructionSource::Inline {
name: "embedded:test/template".to_string(),
content: "INLINE_MARKER_CONTENT".to_string(),
}])
.expect("non-empty");
assert!(block.contains("INLINE_MARKER_CONTENT"));
assert!(block.contains("source=\"embedded:test/template\""));
// Empty inline → skipped just like empty file.
let empty_inline = super::InstructionSource::Inline {
name: "empty".to_string(),
content: " ".to_string(),
}; };
assert!(super::render_instructions_block(&[empty_inline]).is_none()); let one_block = SystemPrompt::Blocks(vec![stable.clone()]);
let with_volatile = SystemPrompt::Blocks(vec![
// Oversize inline → truncated with elided marker. stable,
let big_inline = super::InstructionSource::Inline { SystemBlock {
name: "huge".to_string(), block_type: "text".to_string(),
content: "Y".repeat(200 * 1024), text: "MEMORY_BLOCK ".repeat(2_000),
}; cache_control: None,
let trimmed = super::render_instructions_block(&[big_inline]).expect("non-empty");
assert!(trimmed.contains("[…truncated:"));
// File + Inline 混用,顺序保持。
let tmp = tempdir().expect("tempdir");
let file_path = tmp.path().join("file-first.md");
std::fs::write(&file_path, "FILE_MARKER").unwrap();
let mixed = super::render_instructions_block(&[
file_path.into(),
super::InstructionSource::Inline {
name: "inline-second".to_string(),
content: "INLINE_MARKER".to_string(),
}, },
]) ]);
.expect("non-empty"); assert_eq!(
let file_pos = mixed.find("FILE_MARKER").expect("file rendered"); super::measure_static_prefix(Some(&one_block), &[]),
let inline_pos = mixed.find("INLINE_MARKER").expect("inline rendered"); super::measure_static_prefix(Some(&with_volatile), &[]),
assert!(file_pos < inline_pos, "声明顺序必须保留(File then Inline)"); "volatile world-state blocks are not part of the guarded prefix"
);
}
#[test]
fn relay_artifact_cap_unchanged() {
// The relay keeps its own 100 KiB cap; the instruction ceiling comes
// from `[instruction_budget] file_max_bytes` and must not change how a
// relay artifact is read.
assert_eq!(super::RELAY_ARTIFACT_MAX_BYTES, 100 * 1024);
}
#[test]
fn configured_instructions_render_inside_the_instruction_block() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let present = workspace.join("present.md");
std::fs::write(&present, "CONFIGURED_PRESENT").unwrap();
let empty = workspace.join("empty.md");
std::fs::write(&empty, " \n\n").unwrap();
let missing = workspace.join("missing.md");
let configured = vec![
super::InstructionSource::File(missing),
super::InstructionSource::File(empty),
super::InstructionSource::File(present.clone()),
super::InstructionSource::Inline {
name: "embedded:test".to_string(),
content: "INLINE_PRESENT".to_string(),
},
];
let mut ctx = crate::project_context::load_project_context_with_parents(workspace);
let budgets = crate::project_context::InstructionBudgets::defaults_for(Some(128_000));
let assembly =
crate::project_context::assemble_instruction_block(&mut ctx, &configured, &budgets);
let block = assembly.block.expect("a block");
assert!(block.contains("CONFIGURED_PRESENT"), "{block}");
assert!(block.contains("INLINE_PRESENT"), "{block}");
assert!(block.contains("source=\"embedded:test\""), "{block}");
let present_at = block.find("CONFIGURED_PRESENT").unwrap();
let inline_at = block.find("INLINE_PRESENT").unwrap();
assert!(present_at < inline_at, "declared order is preserved");
assert!(
assembly
.notices
.iter()
.any(|notice| notice.action == crate::project_context::InstructionAction::Skipped),
"missing and empty entries are reported: {:?}",
assembly.notices
);
} }
#[test] #[test]
@@ -93,7 +93,10 @@ async fn get_v1_workspace_instructions_lists_effective_sources() -> Result<()> {
); );
assert_eq!(body["generated_fallback"], false); assert_eq!(body["generated_fallback"], false);
assert!(body["aggregate_budget_bytes"].as_u64().unwrap() > 0); // #6526: the per-scope token ceilings replaced the single byte cap.
assert!(body["global_budget_tokens"].as_u64().unwrap() > 0);
assert!(body["project_budget_tokens"].as_u64().unwrap() > 0);
assert!(body["usable_window_tokens"].as_u64().unwrap() > 0);
handle.abort(); handle.abort();
Ok(()) Ok(())
+15 -4
View File
@@ -890,9 +890,13 @@ pub(super) struct WorkspaceInstructionsResponse {
/// True when no file-based instructions exist anywhere and the /// True when no file-based instructions exist anywhere and the
/// prompt carries the ephemeral generated context instead. /// prompt carries the ephemeral generated context instead.
generated_fallback: bool, generated_fallback: bool,
/// Aggregate byte ceiling applied across instructions and rules at /// Resolved per-scope ceilings in estimated tokens (#6526): the global
/// render (`project_context::MAX_PROJECT_INSTRUCTION_BYTES`). /// scope, the project scope (instructions + rules), the static-prefix
aggregate_budget_bytes: usize, /// guard, and the usable window they are percentages of.
global_budget_tokens: usize,
project_budget_tokens: usize,
static_guard_tokens: usize,
usable_window_tokens: usize,
/// Warnings from the real load pass — includes assembly-level notices /// Warnings from the real load pass — includes assembly-level notices
/// (unimported foreign formats, ignored WHALE.md, constitution parse) /// (unimported foreign formats, ignored WHALE.md, constitution parse)
/// alongside what per-source `warning` fields already carry. /// alongside what per-source `warning` fields already carry.
@@ -937,6 +941,10 @@ pub(super) async fn workspace_instructions(
let workspace = state.workspace.clone(); let workspace = state.workspace.clone();
let home = codewhale_paths::user_home(); let home = codewhale_paths::user_home();
let configured = state.config.read().instructions_paths(); let configured = state.config.read().instructions_paths();
// Budgets are resolved per route window; this route has no model context
// window of its own, so it reports what the loader would use when the
// window is unknown.
let budgets = crate::project_context::active_instruction_budgets(None, None).ok();
let (sources, generated_fallback, warnings, workspace_root) = let (sources, generated_fallback, warnings, workspace_root) =
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
@@ -999,7 +1007,10 @@ pub(super) async fn workspace_instructions(
.map(str::to_string) .map(str::to_string)
.collect(), .collect(),
generated_fallback, generated_fallback,
aggregate_budget_bytes: crate::project_context::MAX_PROJECT_INSTRUCTION_BYTES, global_budget_tokens: budgets.map_or(0, |budgets| budgets.global_tokens),
project_budget_tokens: budgets.map_or(0, |budgets| budgets.project_tokens),
static_guard_tokens: budgets.map_or(0, |budgets| budgets.static_guard_tokens),
usable_window_tokens: budgets.map_or(0, |budgets| budgets.usable_window_tokens),
warnings, warnings,
})) }))
} }
+39
View File
@@ -1111,6 +1111,45 @@ pub async fn run_tui(
app.status_message = Some(notice); app.status_message = Some(notice);
} }
// #6526: standing-instruction losses are reported at session start as a
// system cell. The status line above belongs to auto-resume and would be
// overwritten by the next receipt.
let configured_sources: Vec<crate::prompts::InstructionSource> = config
.instructions_paths()
.into_iter()
.map(Into::into)
.collect();
let instruction_budget_window = crate::route_budget::route_context_window_tokens(
app.api_provider,
app.effective_model_for_budget(),
app.active_route_limits,
) as usize;
// The check walks directories and reads configured files; keep it off the
// UI's async task.
let notice_workspace = app.workspace.clone();
let notice_model = app.effective_model_for_budget().to_string();
let notice = tokio::task::spawn_blocking(move || {
crate::project_context::startup_instruction_notice(
&notice_workspace,
&configured_sources,
Some(instruction_budget_window),
Some(&notice_model),
)
})
.await;
match notice {
Ok(Ok(Some(notice))) => {
app.add_message(HistoryCell::System { content: notice });
}
Ok(Ok(None)) => {}
Ok(Err(error)) => return Err(anyhow::anyhow!(error)),
Err(error) => {
return Err(anyhow::anyhow!(
"instruction check did not complete: {error}"
));
}
}
let session_id = ensure_runtime_session_id(&mut app); let session_id = ensure_runtime_session_id(&mut app);
let transition = let transition =
prepare_offline_queue_transition(&app, &session_id).map_err(anyhow::Error::msg)?; prepare_offline_queue_transition(&app, &session_id).map_err(anyhow::Error::msg)?;
@@ -2,7 +2,7 @@
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Create denied.txt with touch.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Create denied.txt with touch.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"changed":false,"context_updates":0,"description":"frozen: 485d06a920ab","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"frozen: 5fc80eccdc0f","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":150,"output_tokens":15},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}} {"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":150,"output_tokens":15},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}}
@@ -2,7 +2,7 @@
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Tell me a long story.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Tell me a long story.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"changed":false,"context_updates":0,"description":"frozen: 0be12fe15cac","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"frozen: 54c42060c945","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"event":"message_started","index":0} {"event":"message_started","index":0}
@@ -2,7 +2,7 @@
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"What does README.md say?","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"What does README.md say?","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"changed":false,"context_updates":0,"description":"frozen: 0be12fe15cac","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"frozen: 54c42060c945","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":150,"output_tokens":20},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}} {"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":150,"output_tokens":20},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}}
@@ -2,7 +2,7 @@
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Read README.md and NOTES.md.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- NOTES.md (file)\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Read README.md and NOTES.md.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- NOTES.md (file)\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"changed":false,"context_updates":0,"description":"frozen: 0be12fe15cac","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"frozen: 54c42060c945","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":160,"output_tokens":40},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}} {"event":"provider_request_receipt","receipt":{"cache_attribution":"Local prefix changes and wire schema measurements are context for reported usage, not proof that eviction caused a miss or that unreported retries were free.","elapsed_ms":"<masked>","outcome":"completion_unknown","prefix_check":{"outcome":"stable"},"request_index":1,"reused_preparation":false,"transport_retries":0,"turn_id":"<uuid:1>","usage":{"input_tokens":160,"output_tokens":40},"usage_reported":true,"wire_observations":[],"wire_observations_dropped":0}}
@@ -2,7 +2,7 @@
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Say hello.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Say hello.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"changed":false,"context_updates":0,"description":"frozen: 0be12fe15cac","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"frozen: 54c42060c945","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"event":"message_started","index":0} {"event":"message_started","index":0}
@@ -2,7 +2,7 @@
{"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"created_at":"<masked>","event":"turn_started","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Write c0205.txt.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Full Access\nCurrent sandbox posture: full access (sandbox disabled)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Write c0205.txt.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Full Access\nCurrent sandbox posture: full access (sandbox disabled)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"changed":false,"context_updates":0,"description":"frozen: 0be12fe15cac","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false} {"changed":false,"context_updates":0,"description":"frozen: 54c42060c945","event":"prefix_cache_change","last_miss_reason":"","pin_reason":"initial","pinned_combined_hash":"<masked>","stability_pct":100,"system_prompt_changed":false,"tools_changed":false}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"6b321388357ce33f457a866082c4106d7983cdd913658e83ba8cd426b7513ef0","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16189,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only in Full Access when the session sandbox, the network policy and your network grant allow it. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background byLine truncated
{"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"} {"event":"route_dispatched","route":{"auto_model":false,"base_url":"https://api.deepseek.com/beta","billing":{"billing_mode":"metered","dispatched_at":"<timestamp>"},"billing_product":{"kind":"unproven"},"model":"deepseek-v4-pro","provider":"deepseek","provider_identity":"deepseek"},"turn_id":"<uuid:1>"}
{"category":"invalid_input","code":"invalid_input","event":"error","message":"Model not exist.","recoverable":false,"severity":"error"} {"category":"invalid_input","code":"invalid_input","event":"error","message":"Model not exist.","recoverable":false,"severity":"error"}
@@ -1,7 +1,7 @@
{ {
"system_sha256": "8ff744b8ed90564a8c4754ac79cdbaf05df69ed024cc2ec154abfe581ed72d7f", "system_sha256": "bc9eb8c1158d3aadb8de07b759b21bd6795d6dcb4e358ce118ab7ac32fceeea5",
"system_bytes": 9183, "system_bytes": 9234,
"system_blocks": 5, "system_blocks": 4,
"tools_sha256": "be2b1339653cbfba99a14a32777c896e766001ef0e62edca75386e749fb4940d", "tools_sha256": "be2b1339653cbfba99a14a32777c896e766001ef0e62edca75386e749fb4940d",
"tools_bytes": 16189, "tools_bytes": 16189,
"tool_count": 13, "tool_count": 13,
@@ -20,7 +20,7 @@
"execute_tools", "execute_tools",
"tool_search" "tool_search"
], ],
"prefix_sha256": "526cfe8c2589afe41fffc96e4b33c36375c9abfc1a1608fd68c12d83caaadaa7", "prefix_sha256": "2f9915b84316edcf1d12fef830586214807662f7b201bfd7a22ea37af0d4a619",
"masks": [ "masks": [
"<WORKSPACE>", "<WORKSPACE>",
"<HOME>", "<HOME>",
@@ -97,6 +97,20 @@ If you genuinely need column-aligned data because the user asked for a table or
Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed. Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed.
<project_instructions source="project"> <project_instructions source="project">
<!-- global: runtime:mcp-registry-first -->
<instructions source="runtime:mcp-registry-first">
## MCP Registry
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
</instructions>
<!-- project (overrides global where they conflict) -->
# Project Context (Auto-generated, ephemeral) # Project Context (Auto-generated, ephemeral)
> This context was generated in memory by Codewhale. > This context was generated in memory by Codewhale.
@@ -150,19 +164,6 @@ lists.
- platform: <PLATFORM> - platform: <PLATFORM>
- shell: <SHELL> - shell: <SHELL>
<!-- cw:ctx:permissions -->
<instructions source="runtime:mcp-registry-first">
## MCP Registry
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
</instructions>
<!-- cw:ctx:route --> <!-- cw:ctx:route -->
verbosity: default verbosity: default
translation: off translation: off
@@ -1,7 +1,7 @@
{ {
"system_sha256": "8ff744b8ed90564a8c4754ac79cdbaf05df69ed024cc2ec154abfe581ed72d7f", "system_sha256": "bc9eb8c1158d3aadb8de07b759b21bd6795d6dcb4e358ce118ab7ac32fceeea5",
"system_bytes": 9183, "system_bytes": 9234,
"system_blocks": 5, "system_blocks": 4,
"tools_sha256": "c1bb513318e0f4f33100133c7cfdd04c8acbda1280efb353a688b061d5ead264", "tools_sha256": "c1bb513318e0f4f33100133c7cfdd04c8acbda1280efb353a688b061d5ead264",
"tools_bytes": 13689, "tools_bytes": 13689,
"tool_count": 11, "tool_count": 11,
@@ -18,7 +18,7 @@
"write", "write",
"tool_search" "tool_search"
], ],
"prefix_sha256": "d105d912830aa12fb1519e68df9f23c8a13dd8b8efd39daf3ab35545ab71cfcb", "prefix_sha256": "1355b7e1b5a0e760261999ea277218de62d0ea8193c3cf407475462e644b541d",
"masks": [ "masks": [
"<WORKSPACE>", "<WORKSPACE>",
"<HOME>", "<HOME>",
@@ -97,6 +97,20 @@ If you genuinely need column-aligned data because the user asked for a table or
Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed. Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed.
<project_instructions source="project"> <project_instructions source="project">
<!-- global: runtime:mcp-registry-first -->
<instructions source="runtime:mcp-registry-first">
## MCP Registry
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
</instructions>
<!-- project (overrides global where they conflict) -->
# Project Context (Auto-generated, ephemeral) # Project Context (Auto-generated, ephemeral)
> This context was generated in memory by Codewhale. > This context was generated in memory by Codewhale.
@@ -150,19 +164,6 @@ lists.
- platform: <PLATFORM> - platform: <PLATFORM>
- shell: <SHELL> - shell: <SHELL>
<!-- cw:ctx:permissions -->
<instructions source="runtime:mcp-registry-first">
## MCP Registry
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
</instructions>
<!-- cw:ctx:route --> <!-- cw:ctx:route -->
verbosity: default verbosity: default
translation: off translation: off
@@ -1,7 +1,7 @@
{ {
"system_sha256": "cd367daa552060c0b30a60263a509fdd25a0d92c89f24aa9f69c174d52076f3d", "system_sha256": "d7f21fbb26b7f6f5449c9be88ff5fac02e1289b607ec0b8189bae842aa956bd9",
"system_bytes": 8757, "system_bytes": 8808,
"system_blocks": 5, "system_blocks": 4,
"tools_sha256": "be2b1339653cbfba99a14a32777c896e766001ef0e62edca75386e749fb4940d", "tools_sha256": "be2b1339653cbfba99a14a32777c896e766001ef0e62edca75386e749fb4940d",
"tools_bytes": 16189, "tools_bytes": 16189,
"tool_count": 13, "tool_count": 13,
@@ -20,7 +20,7 @@
"execute_tools", "execute_tools",
"tool_search" "tool_search"
], ],
"prefix_sha256": "88bae4628f69e12c76eee2880fc9f93e55fedf7a832a9f42f5dcaf6bbbf6e0a6", "prefix_sha256": "2b90c6212b52f1225cb57a11370be00b2e0c7917f2ddf58602b17c68528d449a",
"masks": [ "masks": [
"<WORKSPACE>", "<WORKSPACE>",
"<HOME>", "<HOME>",
@@ -97,6 +97,20 @@ If you genuinely need column-aligned data because the user asked for a table or
Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed. Progress updates narrate the user's task — what you found, what you are doing next, what you decided — not the harness. Do not narrate tool plumbing: sandboxing, network routing, schema loading, tool search, retries, batching, or which tool you will call. When a gate actually blocks the work and needs the user, say what is blocked and what they can do, in their terms; otherwise just proceed.
<project_instructions source="AGENTS.md"> <project_instructions source="AGENTS.md">
<!-- global: runtime:mcp-registry-first -->
<instructions source="runtime:mcp-registry-first">
## MCP Registry
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
</instructions>
<!-- project (overrides global where they conflict) -->
# Conformance project rules # Conformance project rules
- Keep every change inside the fixture workspace. - Keep every change inside the fixture workspace.
@@ -129,19 +143,6 @@ lists.
- platform: <PLATFORM> - platform: <PLATFORM>
- shell: <SHELL> - shell: <SHELL>
<!-- cw:ctx:permissions -->
<instructions source="runtime:mcp-registry-first">
## MCP Registry
The Registry installs and connects a local MCP server when this session lacks a capability. It is a fallback for a capability you do not have, not a step before ordinary work.
Prefer what is already available, in order: tools already in this catalog, the project's own scripts, tests, and dev tooling, and platform capabilities. Creating a file, reading a fixture, running a repo command, and checking your own output are ordinary work — do them directly.
Reach for the Registry once you have identified a specific capability that no available tool covers and that you would otherwise install or reimplement, such as a document or media converter, access to an external database or service, or a protocol client. Then call `registry_sync` with a `query` naming that capability; it scores the local Registry snapshot host-side and returns at most eight matches, so the full index never enters the conversation. When a returned server plausibly covers that capability, call `start_registry_mcp_server` with its exact name rather than installing or running its package command through the shell. If nothing matches, refine the query once, then continue with local tools.
Both Registry tools are deferred: load one with `tool_search` before its first call, and use the returned schema. If a call instead reports that it only loaded the schema, retry once with that schema. Do not go searching for them for work you can already do.
</instructions>
<!-- cw:ctx:route --> <!-- cw:ctx:route -->
verbosity: default verbosity: default
translation: off translation: off
+82 -12
View File
@@ -76,14 +76,14 @@ controls do not blur together.
root down to the workspace, and puts your global instruction file (such as root down to the workspace, and puts your global instruction file (such as
`~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last
and wins: the workspace file overrides its parents, and project files and wins: the workspace file overrides its parents, and project files
override the global file. A file over 100 KiB is cut to its first 100 KiB override the global file. Global and project instructions are budgeted
and ends with a `[…truncated: N of M bytes omitted …]` marker, and separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)
`/context report` names the file. Instruction and rules files share one below): a scope over its budget drops whole files from the broadest end
48 KiB budget, and instruction files claim it first. Instruction text past first, and a single file larger than the remaining budget keeps its head
the budget is dropped from the front, broadest scope first (the global with a marker that names the bytes and tokens dropped. A file larger than
file, then the repository root), so a file larger than the budget keeps `file_max_bytes` is skipped with a notice, never truncated in place. Every
only its end; a marker gives the bytes dropped. Rules past what is left loss — trimmed, skipped, or refused — reaches the operator, the model, and
are cut from the end. `/context report`.
- **Memory and handoffs** — recalled state. Useful, but lower authority than - **Memory and handoffs** — recalled state. Useful, but lower authority than
constitutions and project instructions. constitutions and project instructions.
@@ -1563,15 +1563,85 @@ instructions = [
Rules: Rules:
- Paths run through `expand_path` so `~` and env vars work. - Paths run through `expand_path` so `~` and env vars work.
- Each file is capped at 100 KiB; oversized files are - These entries are **global-scope** instruction authority: they are charged
truncated with a `[…elided]` marker rather than skipped. to the global budget together with the global `AGENTS.md`/`instructions.md`
- Missing files are skipped with a tracing warning so a stale fallbacks and `~/.claude/CLAUDE.md`.
entry doesn't fail the launch. - A file larger than `[instruction_budget] file_max_bytes` (8 MiB by default)
is skipped with a notice, not truncated. Missing and empty files are skipped
with a notice so a stale entry doesn't fail the launch.
- Only user-owned config, profiles, and managed config may set this array. - Only user-owned config, profiles, and managed config may set this array.
Project config (`<workspace>/.codewhale/config.toml`, or legacy Project config (`<workspace>/.codewhale/config.toml`, or legacy
`<workspace>/.deepseek/config.toml`) ignores `instructions` so a cloned repo `<workspace>/.deepseek/config.toml`) ignores `instructions` so a cloned repo
cannot choose arbitrary local files to place into the prompt. cannot choose arbitrary local files to place into the prompt.
### Instruction budget (`[instruction_budget]`, #6526)
Standing instruction files are assembled into two independent scopes and
budgeted in **estimated tokens** — the same conservative estimator `/context`
uses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.
- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`
fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`
as a last resort, and the `instructions = [...]` array.
- **project** — the repository-root → workspace `AGENTS.md` chain, co-read
foreign files, and the `.codewhale/rules`/`.claude/rules` block.
Each scope is capped at `min(absolute_tokens, percent × usable_window)`, where
the usable window is the route window minus the response ceiling and a safety
reserve of 10% (minimum 2,048 tokens).
```toml
[instruction_budget]
global_budget_tokens = 100000
project_budget_tokens = 100000
global_budget_percent = 25
project_budget_percent = 25
static_prefix_guard_percent = 80
file_max_bytes = 8388608
policy = "trim"
claude_fallback = true
```
| window | usable (approx) | per scope | both scopes | guard 80% |
| --- | --- | --- | --- | --- |
| 32k | 26.8k | 6,700 | 13,400 | 21,400 |
| 128k | 113.1k | 28,275 | 56,550 | 90,500 |
| 1M | 897.9k | 100,000 | 200,000 | 718,300 |
Behaviour:
- **Nothing is trimmed while a source is within its scope budget.**
- A scope over budget drops whole files from the broadest end first — the
root-most ancestor goes first, the workspace file last — and a single file
that alone exceeds the remaining budget keeps its **head** and drops its
tail with a marker carrying bytes and estimated tokens.
- Rules are charged to the project scope after the instruction chain and keep
a head-first trim.
- `file_max_bytes` is a hard ceiling: a larger file is **skipped**, never
trimmed. The same applies to a whitespace-only file and to files beyond the
50-per-directory rules cap.
- `policy = "trim"` (default) trims and reports. `policy = "refuse"` stops the
session when a scope budget or the rules budget would trim; skips stay skips,
because one bad file must not make the project unopenable. The static-prefix
guard is a safety net rather than a promise: it trims, reports, and lets the
session start even under `refuse`.
- `static_prefix_guard_percent` caps the stable prefix (base prompt,
instructions, skills index, tool schemas). When it trips, the largest scope
is trimmed by the overage, the header is recomposed and re-pinned once, and
a notice is recorded. The guard must leave at least 10 points above the two
scope percents.
- `claude_fallback = true` (default): a directory with no `AGENTS.md` (or
codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped `AGENTS.md`
counts as absent; a file already loaded from a wider scope (a symlink) is a
reported duplicate, not a fallback trigger.
- Every loss — trimmed, skipped, refused — is reported as a system cell at
session start, on stderr in `exec`, to the model as a runtime-provenance
message, and per source in `/context report`.
- The table is **user config only**, like `instructions`: project config is
ignored and a cloned repository cannot shrink the global budget or force
`refuse`. An invalid table (unknown key, zero budget, percent outside
1–100, guard without room left) fails the launch.
### Hooks ### Hooks
Hooks are a **TUI runtime feature**. They fire from the interactive TUI and Hooks are a **TUI runtime feature**. They fire from the interactive TUI and
+539
View File
@@ -0,0 +1,539 @@
# Standing instructions: selection, budgets, and visible truncation
Plan for issue #6526 (and the comment at
https://github.com/codewhale-hq/Codewhale/issues/6526#issuecomment-6393297659).
- **Status:** v5 — implemented on this branch (see §10 for what shipped and
where the plan was written against an older tree).
- **Branch:** `fix/instruction-budget-6526` (from `main` at `498dd5a87`, 2026-10-24).
- **Scope:** standing instruction files (`AGENTS.md`, `CLAUDE.md`, `instructions.md`,
the top-level `instructions = [...]` array, `rules/`). Skills are a separate issue (#7073).
- **Orientation:** OpenCode's selection model (AGENTS.md wins, otherwise CLAUDE.md) with
codewhale's own sources preserved; no content truncation while a source is within budget.
## 1. Problem
Today codewhale has four instruction sources, three ceilings, and two different trim
directions — none of them visible to the operator.
### 1.1 Global layer
`load_global_agents_context` tries six candidates in order, first existing wins, and falls
through to the next candidate on a read error (`crates/tui/src/project_context.rs:1000-1033`):
1. `~/.codewhale/AGENTS.md`
2. `~/.agents/AGENTS.md`
3. `~/.deepseek/AGENTS.md` (legacy)
4. `~/.codewhale/instructions.md`
5. `~/.agents/instructions.md`
6. `~/.deepseek/instructions.md`
`~/.claude/CLAUDE.md` is not read today.
### 1.2 Project layer
Per directory, root → workspace:
- `AGENTS.md` (canonical);
- `.agents/AGENTS.md` (always, as a cross-agent fragment, `:196-208`);
- `.codewhale/instructions.md`;
- `.deepseek/instructions.md` (legacy);
- `CLAUDE.md` and other foreign formats only when `project_instruction_imports` enables them
(`crates/tui/src/config.rs:2346-2352`, empty by default);
- `WHALE.md` is ignored and reported as a migration diagnostic.
### 1.3 Rules
`.codewhale/rules/*.md` and `.claude/rules/*.md` (`project_context.rs:341`), plus foreign rules
directories when imports are enabled (`:254-265`). `MAX_RULES_FILES = 50` per directory
(`:390`) silently drops the rest with a `tracing::warn!` only (`:1144-1155`).
### 1.4 Configured extra files
The top-level `instructions = [...]` array (`crates/tui/src/config.rs:2327`, user config only;
the project overlay ignores it). Each entry is capped at 100 KiB and trimmed with
`[…truncated: N of M bytes omitted — …]` (`crates/tui/src/prompts.rs:113`, `:154-164`,
`:464-540`). This is not a `[context]` key: `ContextConfig` only has `project_pack`
(`config.rs:1675`) and deliberately ignores unknown fields (#6516).
### 1.5 Ceilings and trim directions
- Aggregate `MAX_PROJECT_INSTRUCTION_BYTES = 48 * 1024` (`project_context.rs:386`), shared by
instructions and rules.
- Per-file `MAX_CONTEXT_SIZE = 100 * 1024` (`:368`): a larger file is skipped (`TooLarge`,
`:1070-1075`); a whitespace-only file is skipped as `Empty` (`:1078-1082`).
- Instructions are trimmed with `keep_tail_within` (`:397-410`): keep the tail, drop the
front (broadest scope first), marker `[…broader-scope project instructions dropped at the
aggregate budget…]`.
- Rules are trimmed with `keep_head_within` (`:413-426`): keep the head, drop the tail (the
last rule files), marker `[…rules block truncated at the aggregate budget…]` (`:456-470`).
- The global text is merged into the project text (`merge_global_and_project_instructions`,
`:985-999`) before `enforce_project_instruction_budget` runs (`:552`, `:749`). Per-scope
budgets require a pipeline change: keep the scopes separate until each is measured and
trimmed, then merge.
### 1.6 Visibility
`ProjectContext.warnings` appears only in `/context report`
(`crates/tui/src/context_report.rs:560-571`) and the Runtime API source map. Nothing is shown
at session start; the model sees inline markers without sizes or file names.
### 1.7 Measured on a real setup (approximate; markers and labels excluded)
Global `~/.agents/AGENTS.md` = 71,522 B, project `AGENTS.md` = 37,571 B. Assembled 109,093 B;
the 48 KiB tail keeps the project and about 11.3 KiB (16%) of the global; about 58.5 KiB are
dropped from the top. A 26 KiB project leaves ~22.0 KiB (31%) of the global. A 120 KiB project
is skipped by the per-file ceiling and the global is still cut to 49,152 B.
### 1.8 Data model and cache today
`enforce_project_instruction_budget` sees one flattened `String` (`ctx.instructions`) plus
`ctx.rules_block`; the per-file segments (`ancestor_docs: Vec<(PathBuf, String)>`) are local to
the loader and gone by trim time. The project-context cache
(`crates/tui/src/project_context_cache.rs`, wrapper
`load_project_context_with_parents_cached_and_home`) is keyed on files, not on the context
window. Both facts constrain the design: segment-level trimming needs segments kept on
`ProjectContext`, and window-derived budgets must not be baked into a file-keyed cache.
## 2. What we compared
### Claude Code (`code.claude.com/docs/en/memory`)
- Load order, broadest → most specific: managed policy → `~/.claude/CLAUDE.md` → project
(`./CLAUDE.md` or `./.claude/CLAUDE.md`, every ancestor) → `./CLAUDE.local.md`.
- All discovered files are concatenated, order root → cwd; **no size cap, no truncation**.
- `AGENTS.md` is read only when no `CLAUDE.md`/`.claude/CLAUDE.md`/`CLAUDE.local.md` exists in
cwd or above; a setting can switch to both / CLAUDE-only / managed-only.
- Imports `@path` up to 4 hops; `.claude/rules/` and path-scoped rules.
- Only explicit limit: auto memory, first 200 lines or 25 KB.
### OpenCode (`opencode.ai/docs/rules`, source `anomalyco/opencode@dev`)
- Per category: local `AGENTS.md` beats `CLAUDE.md`; global
`~/.config/opencode/AGENTS.md` beats `~/.claude/CLAUDE.md`.
- `instructions` in `opencode.json` adds paths, globs, and remote URLs (5 s timeout), combined
with `AGENTS.md`.
- **No content truncation.** `config/plugin/instruction.ts` reads global + ancestor
`AGENTS.md` + configured sources; `session/instructions.ts` injects each file whole as a
synthetic message `Instructions from: <path>\n<content>`.
- Order: global → project (nearest → farthest) → configured.
### codewhale today
Concatenate global + project + rules, then trim to 48 KiB: instructions keep the tail, rules
keep the head, global is discarded first. The only one of the three that cuts content.
## 3. Decisions
### 3.1 Selection
- **Project, per directory:** `AGENTS.md` wins; if absent, `CLAUDE.md` is the default
fallback (OpenCode parity), controlled by `claude_fallback = true`. `CLAUDE.md` is also read
alongside `AGENTS.md` when `project_instruction_imports` includes `claude`.
| AGENTS.md | CLAUDE.md | `claude` import | `claude_fallback` | read |
| --- | --- | --- | --- | --- |
| loaded | absent | any | any | AGENTS.md |
| loaded | loaded | no | any | AGENTS.md |
| loaded | loaded | yes | any | both |
| absent/skipped | loaded | yes | any | CLAUDE.md (explicit import outranks the fallback flag) |
| absent/skipped | loaded | no | true | CLAUDE.md |
| absent/skipped | loaded | no | false | nothing (explicit opt-out) |
| absent/skipped | absent/skipped | any | any | nothing |
`CLAUDE.md` here means `CLAUDE.md` or `.claude/CLAUDE.md` in the directory;
`CLAUDE.local.md` is Claude-specific and is not imported.
**Skip counts as absent:** if the winning `AGENTS.md` is too large or empty, it is treated
as absent and the next candidate (CLAUDE.md) is tried; the notice names both the skipped
file and the fallback selected. The same rule applies to the global chain: a skipped
candidate falls through to the next one. An **empty project scope after a symlink dedupe**
is not "absent": the content is already loaded as global, so no fallback runs; the notice
says `duplicate`.
- **Global:** the six existing candidates in order, then `~/.claude/CLAUDE.md` as the last
resort when none of the six loads and `claude_fallback = true`. Foreign files never outrank
codewhale's own.
- **All ancestor directories** are read, root → workspace.
- **Symlinks are followed** (current `open_read` behaviour); duplicates are detected by
canonical path (inode only on Unix) and loaded once. A project file symlinked to the global
file is counted as global; the project duplicate is reported, not counted twice.
- `.agents/AGENTS.md` is always a fragment; it is not deduped against a same-directory
`AGENTS.md` unless the canonical path is identical.
### 3.2 Budgets
- Budgets are in **tokens**. Files and the per-file ceiling are in bytes; conversion uses the
engine's token estimator on the assembled text, the same estimator the context report uses.
No `bytes/4` shortcut (it underestimates Cyrillic by 30–50%).
- **Usable window** = `context_window − max_output_tokens − safety_reserve`
(`safety_reserve` = 10% of the window, minimum 2,048 tokens). All percentages are of the
usable window, not the raw window.
- Per-scope budgets, independent:
`effective = min(absolute_tokens, percent × usable_window)`.
Defaults: **100,000 tokens** and **25%** per scope. All values are positive; `0` is invalid
(a finite budget is the point). `percent` is 1–100.
- **Scope assignment:** global scope = the six global candidates + `~/.claude/CLAUDE.md` + the
configured `instructions = [...]` entries (user-level authority). Project scope = the
per-directory chain + fragments + `CLAUDE.md` + rules. Configured entries are therefore
global, not project; rules are project.
- **Static-prefix guard:** the static prefix = base system prompt + tool-use policy +
environment block + instructions + skills index + tool schemas + memory. It must stay under
`static_prefix_guard_percent` (default 80% of the usable window). The guard is the safety
net for non-instruction static content; it is not a substitute for a total-instructions key.
- **No separate "total instructions" key.** A key equal to the sum of the two scope caps
(50%) would be inert; a smaller one would contradict the per-scope decision. The guard
covers the actual risk.
- Effective table (raw windows, before the output/safety reserve; the reserve only lowers the
usable window):
| window | usable (approx) | per scope | both scopes | guard 80% | residual for other static |
| --- | --- | --- | --- | --- | --- |
| 32k | ~26.8k | 6,700 | 13,400 | 21,400 | 8,000 |
| 120k | ~105.8k | 26,450 | 52,900 | 84,600 | 31,700 |
| 128k | ~113.1k | 28,275 | 56,550 | 90,500 | 33,950 |
| 150k | ~132.8k | 33,200 | 66,400 | 106,200 | 39,800 |
| 1M | ~897.9k | 100,000 (10%) | 200,000 (20%) | 718,300 | 518,300 |
The owner's 1M expectation (10% per scope, 20% total) is preserved by the absolute cap.
A 32k window is best-effort. Two thresholds are distinct: guard trimming begins when
non-instruction static content exceeds the residual (~8,000 tokens at 32k); the guard is
`unsatisfiable` only when non-instruction static content alone exceeds the guard
(~21,400 tokens at 32k). In the first case instructions are trimmed by the overage; in the
second they reach zero and the session proceeds (or refuses, under `policy = refuse`).
### 3.3 Per-file ceiling
- `file_max_bytes = 8 MiB` is a memory-safety limit, not truncation. The size is checked with
`metadata()` before reading; a larger file is `skipped`, with a notice naming the file and
the limit. There is no `0`/unlimited value: a hard ceiling must always exist.
- A whitespace-only file is `skipped (empty)`.
- `MAX_RULES_FILES = 50` stays as a safety cap, but its overflow is reported as
`skipped (rules cap)` in the notice and `/context`; it is no longer silent.
- Files are read as a whole up to the ceiling. For files near 8 MiB, tokenization is the
expensive step; the scope budget will trim after tokenization. A byte-side pre-cut is an
allowed optimization, not a behaviour change.
### 3.4 Trim semantics
- `ProjectContext` keeps `instruction_segments: Vec<(PathBuf, String)>` (scope, path, text) in
addition to the joined string. Per-scope trimming operates on segments; the joined string is
rebuilt after trimming.
- Each scope is trimmed only against its own budget; the other scope is untouched.
- Within the project scope, drop whole chain segments from the broadest end first (root-most
ancestors go first, the workspace file last). If a single file alone exceeds the remaining
budget, keep its head, drop its tail with `keep_head_within`, and insert a marker with
bytes/tokens. This reverses today's single-file direction (today `keep_tail_within` keeps
the tail); the change is listed in §9. `keep_tail_within` is no longer used for instructions.
- Within the global scope there is normally one file; the same single-file rule applies.
- Rules are charged to the project scope after project instructions; they keep the current
head-first trim (rules are a flat filename-sorted list, with no nearest-scope hierarchy to
preserve), and the marker and notice carry the numbers.
- The trim amount is `over = content_tokens − budget_tokens`; trim `min(scope_tokens, over)`
from the chosen scope, then re-measure once. There is no loop.
- The marker is included in the budget (as today: `max − marker_len`). The cut point is found
by binary search on UTF-8 boundaries so the result is deterministic.
- If trimming leaves zero source bytes (marker excluded), report `skipped (budget)`, not
`trimmed`. This is reachable when the budget is smaller than the marker or when the static
guard trims a scope to zero.
- If the static guard trips, trim the largest instruction scope first, by `over`; re-render
once, re-measure once. If both scopes reach zero and the guard is still exceeded, emit
`static_guard_unsatisfied` and continue (policy `trim`) or stop (policy `refuse`). There is
no retry loop and only one notice per scope per session.
### 3.5 Policy
- Default: `trim` + warning in the chat. `refuse` remains an opt-in value for operators who
prefer a hard stop. `refuse` stops on **trim** losses (scope budget or static guard) and on
the unsatisfiable guard; **skips** (file ceiling, empty file, rules cap) remain skips and are
reported, because a single bad file must not make the project unopenable.
- The `warn` value is removed; "warn" is what `trim` does.
- Validation rejects unknown policy values, non-positive budgets, percents outside 1–100, a
guard outside 1–100, and any config where `guard − (global_percent + project_percent) < 10`;
the remaining 10 points are the minimum room for non-instruction static content.
### 3.6 Notice contract
| Trigger | Action | User | Model |
| --- | --- | --- | --- |
| file > `file_max_bytes` | skip | startup line | first-turn synthetic |
| empty file | skip | startup line | first-turn synthetic |
| rules cap 50 | skip files | startup line | first-turn synthetic |
| duplicate via symlink | skip duplicate | startup line | first-turn synthetic |
| scope > budget, `trim` | trim that scope | startup line + numbers | first-turn synthetic + marker |
| zero source bytes left | skip | "skipped", not "trimmed" | same |
| static guard, `trim` | trim largest scope by `over` | startup line | first-turn synthetic |
| static guard unsatisfiable | proceed (trim) / stop (refuse) | named line | first-turn synthetic |
| `refuse` policy on trim | stop | named error | session does not start |
| all within budgets | no notice | no | no |
Fields: `path`, `scope` (`global`/`project`), `source` (`agents`/`claude`/`instructions`/
`configured`/`rules`), `action` (`kept`/`trimmed`/`skipped`/`refused`), `bytes_kept`,
`bytes_dropped`, `tokens_kept` (estimated), `tokens_dropped`, `limit_kind` (`scope_budget`/
`static_guard`/`file_ceiling`/`empty_file`/`rules_cap`/`rules_budget`/`duplicate_path`), and
`fallback_selected` when a skipped file was replaced by a lower-precedence candidate.
Channels:
- **User:** a system history cell at session start (`app.add_message(HistoryCell::System {
content })`), not the `startup_notice` status-line field (that one belongs to auto-resume and
would be overwritten); stderr for headless/`exec` (text output). The notice is recomputed
deterministically from files + config, so a resumed session states the same losses.
- **Model:** a first-turn synthetic runtime-provenance message (the LSP-diagnostics mechanism;
see §10), which is part of the logged transcript.
- **`/context report`:** per-source action and budget, alongside the existing warnings at
`context_report.rs:560`.
## 4. Config surface
Top-level `instructions = [...]` stays as is (array). New table **`[instruction_budget]`**
(not `[instructions]`, which would collide with the array; not `[context]`, which ignores
unknown keys and would hide typos):
```toml
[instruction_budget]
global_budget_tokens = 100000 # positive
project_budget_tokens = 100000 # positive
global_budget_percent = 25 # 1..=100
project_budget_percent = 25 # 1..=100
static_prefix_guard_percent = 80 # 1..=100
file_max_bytes = 8388608 # hard ceiling, positive
policy = "trim" # trim | refuse
claude_fallback = true # AGENTS.md wins; otherwise CLAUDE.md
```
- `deny_unknown_fields` on this table so a typo inside it fails loud.
- The **project overlay ignores this table** (like `instructions`), so a cloned repository
cannot shrink the user's global budget or force `refuse`. The overlay/base merge happens at
`config.rs:10625-10628`; the new table follows the `instructions` rule there (user config
only).
- Because the top-level config is tolerant of unknown tables (#6516), a misspelled table name
(`[instruction_budjet]`) would be ignored; `codewhale doctor` inspects the raw TOML before
deserialization and warns about unknown `instruction*` tables and keys.
## 5. Implementation plan
1. **New assembly order** in `crates/tui/src/project_context.rs`:
`assemble_instruction_scopes(raw: &ProjectContext, configured: &[PathBuf], budgets:
&InstructionBudgets)` reads configured entries fresh (they are user config and are not in
the cache key), joins them to the global scope, applies per-scope trims, then merges with
labels. Imports are already baked into the raw cache; budgets are passed separately. The
new function owns the split; the configured entries move out of
`prompts.rs::render_instructions_block`, so there is exactly one owner and one charge
(global).
2. **Cache**: `project_context_cache` keeps **raw segments** keyed by the existing file
candidate set (`project_context_cache_candidate_paths`, `project_context.rs:754-800`).
`apply_instruction_budgets` runs after cache lookup, per request/window; configured
`instructions` entries are read after cache lookup, so a change there is picked up without
cache invalidation; `InstructionSource::Inline` stays outside the cache. A window change
must not invalidate the content cache, and a content change must not reuse trimmed text.
3. **`crates/tui/src/project_context.rs`**
- `ProjectContext` gains `instruction_segments: Vec<InstructionSegment>` (scope, path,
text); the joined string is rebuilt after trimming.
- Delete both `enforce_project_instruction_budget` call sites (`:552`, `:749`) and replace
them with `apply_instruction_budgets`, called by the assembly after cache lookup.
- `load_global_agents_context`: add `~/.claude/CLAUDE.md` as the last candidate; keep the
six existing candidates and fall-through; report the selected fallback.
- `load_dir_instructions` / `active_fragment_candidates`: implement the truth table from
§3.1 (skip-as-absent, explicit import co-reads, symlink dedupe).
- `apply_instruction_budgets`: per-scope budgets; segments dropped whole; `keep_head_within`
for a single oversized file and the rules block; `keep_tail_within` no longer used for
instructions.
- `MAX_RULES_FILES` overflow → structured warning, not only `tracing::warn!`.
- Module-level known-limitations note (repo rule) describing the estimator and trim order.
4. **`crates/tui/src/prompts.rs`**
- `INSTRUCTIONS_FILE_MAX_BYTES` from `file_max_bytes`; split the relay-artifact cap from
the instruction cap so `policy = refuse` does not change relay behaviour.
`render_instructions_block` renders only the assembled instruction block (no configured
entries).
5. **Static guard**: `prompts.rs` exposes `measure_static_prefix(&SystemPrompt, &[ToolSpec])`;
`core/engine/preview.rs` (`compose_stable_system_prompt` + `system_prompt_text`) calls
`enforce_static_prefix_guard` after the stable prefix is composed and the tool registry is
known. The guard covers the **stable** prefix only (base prompt, tool policy, instructions,
skills index, tool schemas); volatile memory/environment blocks are excluded (memory has
its own budget). On trim, rebuild the instruction block once, recompose, re-measure once;
if both scopes are zero and the stable prefix is still over, return `Unsatisfiable`.
6. **Startup notice**
- Emit the user line as a `HistoryCell::System` at session start (TUI), stderr
(headless/exec), and a session event (Runtime API); re-emit on resume from the persisted
event. Do not reuse `startup_notice` (auto-resume status line).
7. **`crates/tui/src/context_report.rs`**
- Show per-source action, bytes/tokens, budget and limit kind.
8. **Docs and registry**
- `docs/CONFIGURATION.md`, `docs/zh_hans/CONFIGURATION.md` (mirror, or an explicit deferral
note with the sync date), `config.example.toml` (replace the fixed 48 KiB comment),
`CHANGELOG.md`, and a `docs/features.toml` row with id/name/summary/status/surfaces/
since/flag/docs/owner (the registry test fails on drift).
9. **Tests** (one per branch, plus the boundary cases):
- `agents_md_wins_over_claude_md`
- `claude_md_is_default_fallback`
- `claude_fallback_can_be_disabled`
- `claude_import_reads_when_agents_absent_and_fallback_disabled`
- `explicit_claude_import_reads_both`
- `agents_md_skipped_falls_back_to_claude`
- `global_claude_is_last_resort_after_instructions_md`
- `symlinked_duplicate_is_loaded_once` (canonical path; no fallback)
- `no_trim_within_scope_budget`
- `per_scope_budgets_are_independent`
- `trim_keeps_the_nearest_scope` (segments)
- `single_oversized_file_keeps_the_head`
- `zero_bytes_left_is_reported_as_skipped`
- `rules_share_the_project_scope_budget`
- `rules_block_is_head_trimmed_with_numbers`
- `static_guard_trims_by_the_overage`
- `static_guard_unsatisfied_terminates`
- `guard_trim_begins_at_residual`
- `unsatisfiable_only_above_guard`
- `static_prefix_guard_measures_tool_schemas`
- `stable_prefix_guard_ignores_memory_growth`
- `cache_returns_untrimmed_segments_across_budgets`
- `configured_instructions_are_read_fresh`
- `inline_instructions_stay_outside_cache`
- `relay_artifact_cap_unchanged`
- `refuse_policy_stops_on_trim_but_not_on_skip`
- `file_over_ceiling_is_skipped_not_trimmed`
- `file_max_bytes_zero_is_rejected`
- `empty_file_is_skipped`
- `rules_cap_is_reported`
- `unknown_window_uses_128k`
- `percent_and_guard_bounds_are_validated`
- `project_overlay_cannot_set_global_budget`
- `instructions_array_counts_toward_global`
- `startup_notice_is_a_system_cell_and_stderr`
- `model_notice_is_a_session_event`
- `resumed_session_reemits_notice`
- `context_report_shows_actions_and_budgets`
- update the existing 48 KiB tests to the new defaults.
## 6. Acceptance criteria
- Default behaviour: no content is trimmed while a source is within its scope budget; the
only losses are scope-budget trims, static-guard trims, and skips (file ceiling, empty,
rules cap) — each with a notice.
- `AGENTS.md` selection and the CLAUDE fallback match the truth table, including
skip-as-absent and `claude_fallback = false`; `project_instruction_imports` still works for
co-reading.
- Global and project budgets are independent; a huge project does not shrink the global; rules
are charged to project; configured `instructions` entries are charged to global.
- Any loss (`trimmed`/`skipped`/`refused`) is reported to the user, to the model, and in
`/context`, with path, scope, source, bytes, tokens and limit kind.
- No silent drop remains: rules cap, empty files, fall-through substitution, symlink
duplicates and per-file skips are all in the contract; the guard has a terminal
unsatisfiable branch and trims by a defined overage.
- All existing tests pass; the new tests above cover every branch; the old 48 KiB tests are
updated.
## 7. Risks
- **Prompt-cache and cost:** a larger static prefix changes the cached prefix and increases
cost per request; the PR must include a measured estimate.
- **Upstream acceptance:** the maintainers said the absolute ceiling is their decision
(#6526); 100k tokens per scope is 6–8× the current 48 KiB. Present the defaults and the
knobs, and be ready to keep the current default.
- **CLAUDE fallback is a policy change:** a repo with only `CLAUDE.md` previously got no
project instructions; now it does. Migration note and `claude_fallback = false` escape.
- **Small windows:** at 32k the residual is ~8,000 tokens for skills/tool schemas; exceeding
it starts guard trimming, and only non-instruction static content above ~21,400 tokens is
`unsatisfiable`. The PR should include a 32k measurement and may lower the default percents
for small windows.
- **Estimator drift:** tokens are estimated, not counted; the notice must say "estimated"
where it matters.
- **Refactor size:** moving configured `instructions` into the assembly and keeping segments
on `ProjectContext` touches the loader, cache and prompt builder; the PR should be split
into a behaviour-preserving refactor and the budget change if maintainers prefer.
## 8. Out of scope
- Skills index budgets and truncation (#7073).
- Skill installation/disable policy (#7066).
- Compaction engine changes.
- Moving instructions from the system prompt into a synthetic message (decided against for
this PR).
## 9. PR framing
- Reference OpenCode's selection model with source links (`opencode.ai/docs/rules`,
`anomalyco/opencode` `config/plugin/instruction.ts`, `session/instructions.ts`) and Claude
Code's no-truncation model (`code.claude.com/docs/en/memory`).
- Include the measured evidence from §1.7, the effective-budget table from §3.2, and the
cost/cache estimate.
- State the behaviour changes (CLAUDE fallback; 48 KiB → per-scope budgets; a single
oversized file now keeps its head instead of its tail; configured `instructions` entries
move from `prompts.rs` rendering into the instruction assembly; the guard covers the stable
prefix only) and the migration path (`claude_fallback = false`, `[instruction_budget]`).
- Use `Refs #6526`, not `fix`/`close`. Maintainers land verified slices on `main`; this branch
is a contributor branch, not a maintainer PR branch.
## 10. Implementation notes (v5)
What landed, and where it differs from the plan above. The differences are
places where the plan cited a tree that has since changed, or where the plan's
mechanism did not exist; none of them changes the behaviour contract in §3.
- **Where the code lives.** Per-scope budgets, segments, notices, trim
markers, and the static-guard decision are in
`crates/tui/src/project_context/budget.rs`. Loader selection and the
assembly are in `crates/tui/src/project_context.rs`; `types.rs` carries the
segments and notices on `ProjectContext`. Tests for the behaviour named in
§5.9 are in `project_context/instruction_budget_tests.rs` (selection, skips,
trims, rules, configured entries, `refuse`), `budget.rs` (budget math and the
guard decision), `prompts.rs` (stable-prefix measurement, configured-entry
rendering), `lib.rs` (project overlay) and `config.rs`.
- **The "first-turn synthetic message" mechanism** named in §3.6 does not exist
in this tree (`crates/tui/src/core/agent.rs` and `announce_active_skill` are
gone). The equivalent is the runtime-provenance synthetic message the LSP
diagnostics path already uses
(`runtime_text_message_with_turn_metadata(..., UserInputProvenance::Runtime)`),
delivered once from turn preparation. It is part of the transcript, so it is
logged (model-visible ⇒ logged) and a resumed session already has it in
history — that replaces the separate persisted event.
- **The static guard** runs in turn preparation rather than
`core/engine/preview.rs`, because that is the first point where the pinned
prefix and the real tool catalog are known together. It measures the first
system block plus the tool schemas (`prompts::measure_static_prefix`), trims
the largest scope by the overage, recomposes and re-pins once under a
declared reason, and classifies the result with the pure
`static_guard_outcome`. `compose_stable_system_prompt` now has an
assembly-returning sibling used by the guard.
- **`policy = "refuse"`** is enforced at session start (TUI and `exec`) by
`project_context::startup_instruction_notice`, which returns an error for a
trim loss or a rules-budget loss. Skips stay skips. The engine itself never
refuses: by the time it runs, the session has already started, and the guard
reports `static_guard_unsatisfied` and continues. **Known limit:** the
embed/ACP/app-server surfaces do not run the startup gate, so `refuse` is a
no-op there — they trim and report like `trim`.
- **Symlinks.** §3.1 assumed the project loader follows them. It does not:
`fs_confined::open_read` refuses a link that leaves the scope root, so a
project file linked to the user's global file is now recognised as a
**duplicate** before the read (the content is already loaded from the wider
scope), and any other symlinked instruction file is *skipped with a notice*
rather than followed. That refusal is pre-existing sandbox behaviour; this
change only makes the duplicate case honest instead of an unreadable-file
error.
- **Foreign fragments** (`.cursorrules`, `.clinerules`, …) keep their own
bounded-fragment cap and are not charged to the project scope; they are
reported in `/context` and named as a known limitation in `budget.rs`.
- **Window used by the startup line.** The TUI/exec startup notice resolves
budgets for the route window they are about to launch with
(`route_context_window_tokens` from the app's effective model, or from the
validated CLI route in `exec`); the 128k default is only a fallback for a
caller that has no route. A first pass used that default unconditionally,
which could report a trim that would not happen and, under
`policy = "refuse"`, stop a session that would have fitted.
- **The user/model channels** are: a `HistoryCell::System` cell at TUI start,
`eprintln!` for `exec` text output, one runtime-provenance transcript
message for the model, and per-source rows in `/context report`. The notice
is recomputed deterministically from files + config, so a resumed session
states the same losses.
- **`/context report`** rows cover the file-based sources and the resolved
budgets; the TUI variant passes no configured `instructions` paths (the App
does not hold the config), so configured entries appear only in the headless
report and in the startup notice. `exec` prints the notice for text output
only; a JSON consumer reads the same losses from `/context report`.
- **Build note for this environment.** The shared cargo registry and target
directory are read-only inside the session sandbox, so the verification
build used an isolated `CARGO_HOME`/target under `/tmp` (`cargo check -p
codewhale-tui`), not the shared target dir. The `codewhale-tui` test binary
is the long pole: full codegen for that crate did not finish inside this
session's time budget, so test *execution* is the one gate still open here;
the crate and its `cfg(test)` code were typechecked instead.
+4 -1
View File
@@ -29,7 +29,10 @@ Codewhale 有多个指令层级(instruction surfaces)。它们刻意保持
- **内置全局宪章(Bundled global Constitution)**——编译进二进制的基础规则。它是每个会话的默认底线。 - **内置全局宪章(Bundled global Constitution)**——编译进二进制的基础规则。它是每个会话的默认底线。
- **用户全局宪章(User-global constitution)**——常规引导式设置的产物。用 `/constitution` 或 `/setup` 管理;Codewhale 把结构化数据存放在 `$CODEWHALE_HOME/constitution.json`(默认 `~/.codewhale/constitution.json`),并渲染成独立的 `<codewhale_user_constitution>` 散文块(prose block)。它可以表达偏好和停止条件,但不会改变运行时审批策略、沙箱、shell、网络、信任或 MCP 权限。 - **用户全局宪章(User-global constitution)**——常规引导式设置的产物。用 `/constitution` 或 `/setup` 管理;Codewhale 把结构化数据存放在 `$CODEWHALE_HOME/constitution.json`(默认 `~/.codewhale/constitution.json`),并渲染成独立的 `<codewhale_user_constitution>` 散文块(prose block)。它可以表达偏好和停止条件,但不会改变运行时审批策略、沙箱、shell、网络、信任或 MCP 权限。
- **仓库本地宪章(Repo-local constitution)**——可选的 `.codewhale/constitution.json` 项目策略,见下文。 - **仓库本地宪章(Repo-local constitution)**——可选的 `.codewhale/constitution.json` 项目策略,见下文。
- **`AGENTS.md`**——跨智能体**项目指令**(散文)。这是"智能体应如何在这个仓库工作"的规范文件。运行 `/init` 生成一份。为其他工具编写的指令文件(`CLAUDE.md`、`.claude/instructions.md`、`.cursorrules`、`.github/copilot-instructions.md` 等)只有在你通过顶层配置项 `project_instruction_imports` 选择导入时才会被读取,例如 `["claude"]`、`["cursor"]` 或 `["all"]`(见 `config.example.toml`);发现未加载的此类文件时,Codewhale 会给出提示。 - **`AGENTS.md`**——跨智能体**项目指令**(散文)。这是"智能体应如何在这个仓库工作"的规范文件。运行 `/init` 生成一份。为其他工具编写的指令文件(`CLAUDE.md`、`.claude/instructions.md`、`.cursorrules`、`.github/copilot-instructions.md` 等)默认不读取;通过顶层配置项 `project_instruction_imports` 选择导入,例如 `["claude"]`、`["cursor"]` 或 `["all"]`(见 `config.example.toml`);发现未加载的此类文件时,Codewhale 会给出提示。**例外(`#6526`)**:某目录没有 `AGENTS.md`(也没有 Codewhale 自己的 `instructions.md`)时,默认回退读取该目录的 `CLAUDE.md`;`claude_fallback = false` 可关闭。
> **说明(2026-10-25)**:指令预算表 `[instruction_budget]`(#6526)——按 `global` / `project` 两个作用域分别用**估算 token** 设上限,并在会话开始时报告每一次裁剪、跳过和拒绝——尚未翻译到本文档。权威说明见英文版 `docs/CONFIGURATION.md` 的 “Instruction budget” 一节;`config.example.toml` 里有完整示例。
> The `[instruction_budget]` section (per-scope token budgets with a trim/skip/refuse notice) is not translated yet; see the English `docs/CONFIGURATION.md`.
- **记忆与交接(Memory and handoffs)**——被召回的状态。有用,但权威低于宪章和项目指令。 - **记忆与交接(Memory and handoffs)**——被召回的状态。有用,但权威低于宪章和项目指令。
### 管理用户全局宪章(`/setup` 和 `/constitution`) ### 管理用户全局宪章(`/setup` 和 `/constitution`)