@@ -323,6 +323,33 @@ pub struct GitWorktreeConfig {
323323 pub allow_path_outside_sandbox : Option < bool > ,
324324}
325325
326+ /// Best-effort workspace diagnostic logging. These limits apply only to the
327+ /// inspectable `.system_generated/logs/` files; SQLite remains the durable
328+ /// source of truth for conversations and run state.
329+ #[ derive( Debug , Deserialize , Serialize , Clone , Default ) ]
330+ pub struct LoggingConfig {
331+ /// Disable file-backed diagnostic logging entirely. Defaults to enabled.
332+ pub enabled : Option < bool > ,
333+ /// Active-file byte limit for `conversation.jsonl`.
334+ pub conversation_max_bytes : Option < u64 > ,
335+ /// Active-file byte limit for `runtime.log`.
336+ pub runtime_max_bytes : Option < u64 > ,
337+ /// Number of rotated files retained for each diagnostic log.
338+ pub retained_generations : Option < usize > ,
339+ /// Aggregate byte cap for recognized diagnostic log files in one workspace.
340+ pub max_total_bytes : Option < u64 > ,
341+ }
342+
343+ /// Fully bounded diagnostic-log settings consumed by the logging actor.
344+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
345+ pub struct EffectiveLoggingConfig {
346+ pub enabled : bool ,
347+ pub conversation_max_bytes : u64 ,
348+ pub runtime_max_bytes : u64 ,
349+ pub retained_generations : usize ,
350+ pub max_total_bytes : u64 ,
351+ }
352+
326353#[ derive( Debug , Deserialize , Serialize , Default , Clone ) ]
327354pub struct AppConfig {
328355 pub restrict_to_workspace : Option < bool > ,
@@ -351,6 +378,8 @@ pub struct AppConfig {
351378 pub multi_tenant_edge : Option < MultiTenantEdgeConfig > ,
352379 /// When `enabled`, `web_search` / `web_fetch` use [Jina Reader](https://r.jina.ai/) and search (`s.jina.ai`).
353380 pub jina : Option < JinaConfig > ,
381+ /// Bounded, file-backed diagnostic logs under `.system_generated/logs/`.
382+ pub logging : Option < LoggingConfig > ,
354383 pub harness : Option < HarnessConfig > ,
355384 /// Named agent definitions (`[agents.<name>]` in config.toml). Top-level alias for
356385 /// `harness.agents` when the user keeps agents in the root of config.toml.
@@ -518,6 +547,56 @@ impl AppConfig {
518547 . or_else ( || self . terminal . as_ref ( ) . and_then ( |t| t. max_tool_output_chars ) )
519548 }
520549
550+ /// Resolved bounds for file-backed diagnostic logs. Invalid or missing
551+ /// values always resolve to bounded settings; they can never enable
552+ /// unbounded workspace log growth.
553+ pub fn effective_logging_config ( & self ) -> EffectiveLoggingConfig {
554+ const DEFAULT_CONVERSATION_MAX_BYTES : u64 = 20 * 1024 * 1024 ;
555+ const DEFAULT_RUNTIME_MAX_BYTES : u64 = 10 * 1024 * 1024 ;
556+ const DEFAULT_RETAINED_GENERATIONS : usize = 2 ;
557+ const DEFAULT_TOTAL_MAX_BYTES : u64 = 90 * 1024 * 1024 ;
558+ const MIN_ACTIVE_FILE_BYTES : u64 = 256 ;
559+ const MAX_ACTIVE_FILE_BYTES : u64 = 512 * 1024 * 1024 ;
560+ const MAX_RETAINED_GENERATIONS : usize = 32 ;
561+ const MAX_TOTAL_BYTES : u64 = 1024 * 1024 * 1024 ;
562+
563+ let logging = self . logging . as_ref ( ) ;
564+ let bounded_bytes = |configured : Option < u64 > , default : u64 | {
565+ configured
566+ . filter ( |bytes| * bytes >= MIN_ACTIVE_FILE_BYTES )
567+ . map ( |bytes| bytes. min ( MAX_ACTIVE_FILE_BYTES ) )
568+ . unwrap_or ( default)
569+ } ;
570+ let conversation_max_bytes = bounded_bytes (
571+ logging. and_then ( |config| config. conversation_max_bytes ) ,
572+ DEFAULT_CONVERSATION_MAX_BYTES ,
573+ ) ;
574+ let runtime_max_bytes = bounded_bytes (
575+ logging. and_then ( |config| config. runtime_max_bytes ) ,
576+ DEFAULT_RUNTIME_MAX_BYTES ,
577+ ) ;
578+ let retained_generations = logging
579+ . and_then ( |config| config. retained_generations )
580+ . unwrap_or ( DEFAULT_RETAINED_GENERATIONS )
581+ . min ( MAX_RETAINED_GENERATIONS ) ;
582+ let minimum_total = conversation_max_bytes. saturating_add ( runtime_max_bytes) ;
583+ let default_total = DEFAULT_TOTAL_MAX_BYTES . max ( minimum_total) ;
584+ let max_total_bytes = logging
585+ . and_then ( |config| config. max_total_bytes )
586+ . filter ( |bytes| * bytes >= minimum_total)
587+ . map ( |bytes| bytes. min ( MAX_TOTAL_BYTES ) )
588+ . filter ( |bytes| * bytes >= minimum_total)
589+ . unwrap_or ( default_total) ;
590+
591+ EffectiveLoggingConfig {
592+ enabled : logging. and_then ( |config| config. enabled ) . unwrap_or ( true ) ,
593+ conversation_max_bytes,
594+ runtime_max_bytes,
595+ retained_generations,
596+ max_total_bytes,
597+ }
598+ }
599+
521600 /// At least one inbound channel other than terminal (API, Slack, or Email).
522601 pub fn has_non_terminal_inbound_channel ( & self ) -> bool {
523602 let api_on = self
@@ -1584,6 +1663,80 @@ pub struct EmailConfig {
15841663mod tests {
15851664 use super :: * ;
15861665
1666+ #[ test]
1667+ fn logging_config_uses_bounded_defaults ( ) {
1668+ let config: AppConfig = toml:: from_str ( "" ) . expect ( "parse empty config" ) ;
1669+
1670+ assert_eq ! (
1671+ config. effective_logging_config( ) ,
1672+ EffectiveLoggingConfig {
1673+ enabled: true ,
1674+ conversation_max_bytes: 20 * 1024 * 1024 ,
1675+ runtime_max_bytes: 10 * 1024 * 1024 ,
1676+ retained_generations: 2 ,
1677+ max_total_bytes: 90 * 1024 * 1024 ,
1678+ }
1679+ ) ;
1680+ }
1681+
1682+ #[ test]
1683+ fn logging_config_parses_explicit_bounded_values ( ) {
1684+ let config: AppConfig = toml:: from_str (
1685+ r#"
1686+ [logging]
1687+ enabled = false
1688+ conversation_max_bytes = 1024
1689+ runtime_max_bytes = 2048
1690+ retained_generations = 3
1691+ max_total_bytes = 4096
1692+ "# ,
1693+ )
1694+ . expect ( "parse logging config" ) ;
1695+
1696+ assert_eq ! (
1697+ config. effective_logging_config( ) ,
1698+ EffectiveLoggingConfig {
1699+ enabled: false ,
1700+ conversation_max_bytes: 1024 ,
1701+ runtime_max_bytes: 2048 ,
1702+ retained_generations: 3 ,
1703+ max_total_bytes: 4096 ,
1704+ }
1705+ ) ;
1706+ }
1707+
1708+ #[ test]
1709+ fn logging_config_invalid_values_stay_bounded ( ) {
1710+ let config: AppConfig = toml:: from_str (
1711+ r#"
1712+ [logging]
1713+ conversation_max_bytes = 0
1714+ runtime_max_bytes = 999999999999
1715+ retained_generations = 999
1716+ max_total_bytes = 1
1717+ "# ,
1718+ )
1719+ . expect ( "parse logging config" ) ;
1720+ let effective = config. effective_logging_config ( ) ;
1721+
1722+ assert_eq ! ( effective. conversation_max_bytes, 20 * 1024 * 1024 ) ;
1723+ assert_eq ! ( effective. runtime_max_bytes, 512 * 1024 * 1024 ) ;
1724+ assert_eq ! ( effective. retained_generations, 32 ) ;
1725+ assert_eq ! ( effective. max_total_bytes, 532 * 1024 * 1024 ) ;
1726+ }
1727+
1728+ #[ test]
1729+ fn logging_config_rejects_integer_overflow ( ) {
1730+ let parsed = toml:: from_str :: < AppConfig > (
1731+ r#"
1732+ [logging]
1733+ conversation_max_bytes = 18446744073709551616
1734+ "# ,
1735+ ) ;
1736+
1737+ assert ! ( parsed. is_err( ) ) ;
1738+ }
1739+
15871740 #[ test]
15881741 fn harness_execution_toml_roundtrip ( ) {
15891742 let s = r#"
0 commit comments