zwarm 3.10.2__py3-none-any.whl → 3.10.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
zwarm/cli/interactive.py CHANGED
@@ -269,10 +269,10 @@ def cmd_ls(manager):
269
269
  task_preview = s.task[:23] + "..." if len(s.task) > 26 else s.task
270
270
  updated = time_ago(s.updated_at)
271
271
 
272
- # Short model name (e.g., "gpt-5.1-codex-mini" -> "codex-mini")
272
+ # Short model name (e.g., "gpt-5.2-codex" -> "5.2-codex")
273
273
  model_short = s.model or "?"
274
274
  if "codex" in model_short.lower():
275
- # Extract codex variant: gpt-5.1-codex-mini -> codex-mini
275
+ # Extract codex variant: gpt-5.2-codex -> 5.2-codex
276
276
  parts = model_short.split("-")
277
277
  codex_idx = next((i for i, p in enumerate(parts) if "codex" in p.lower()), -1)
278
278
  if codex_idx >= 0:
zwarm/cli/main.py CHANGED
@@ -838,19 +838,17 @@ def init(
838
838
  console.print(" [dim]These control the underlying Codex CLI that runs executor sessions[/]\n")
839
839
 
840
840
  console.print(" Available models:")
841
- console.print(" [cyan]1[/] gpt-5.2-codex [dim]- GPT-5.2 Codex, balanced (Recommended)[/]")
841
+ console.print(" [cyan]1[/] gpt-5.2-codex [dim]- GPT-5.2 Codex, fast and balanced (Recommended)[/]")
842
842
  console.print(" [cyan]2[/] gpt-5.2 [dim]- GPT-5.2 with extended reasoning[/]")
843
- console.print(" [cyan]3[/] gpt-5.1-codex [dim]- GPT-5.1 Codex (legacy)[/]")
844
843
 
845
844
  model_choice = typer.prompt(
846
- " Select model (1-3)",
845
+ " Select model (1-2)",
847
846
  default="1",
848
847
  type=str,
849
848
  )
850
849
  model_map = {
851
850
  "1": "gpt-5.2-codex",
852
851
  "2": "gpt-5.2",
853
- "3": "gpt-5.1-codex",
854
852
  }
855
853
  codex_model = model_map.get(model_choice, model_choice)
856
854
  if model_choice not in model_map:
@@ -1668,7 +1666,7 @@ def session_start(
1668
1666
  $ zwarm session start "Fix the bug in auth.py"
1669
1667
 
1670
1668
  [dim]# With specific model[/]
1671
- $ zwarm session start "Refactor the API" --model gpt-5.1-codex-max
1669
+ $ zwarm session start "Refactor the API" --model gpt-5.2-codex
1672
1670
 
1673
1671
  [dim]# Web search is always available[/]
1674
1672
  $ zwarm session start "Research latest OAuth2 best practices"
zwarm/cli/pilot.py CHANGED
@@ -83,22 +83,14 @@ class ChoogingSpinner:
83
83
  # Context window sizes for different models (in tokens)
84
84
  # These are for the ORCHESTRATOR LLM, not the executors
85
85
  MODEL_CONTEXT_WINDOWS = {
86
- # OpenAI models
86
+ # OpenAI models (via Codex CLI)
87
87
  "gpt-5.2-codex": 200_000,
88
88
  "gpt-5.2": 200_000,
89
- "gpt-5.1-codex": 200_000,
90
- "gpt-5.1-codex-mini": 200_000,
91
- "gpt-5": 200_000,
92
- "gpt-5-mini": 200_000,
93
- "o3": 200_000,
94
- "o3-mini": 200_000,
95
- # Claude models (if used as orchestrator)
96
- "claude-sonnet": 200_000,
97
- "claude-opus": 200_000,
98
- "claude-haiku": 200_000,
89
+ # Claude models (via Claude CLI)
99
90
  "sonnet": 200_000,
100
91
  "opus": 200_000,
101
- "haiku": 200_000,
92
+ "claude-sonnet": 200_000,
93
+ "claude-opus": 200_000,
102
94
  # Fallback
103
95
  "default": 128_000,
104
96
  }
@@ -1080,7 +1072,7 @@ def _run_pilot_repl(
1080
1072
  renderer.status("")
1081
1073
 
1082
1074
  # Get model from orchestrator if available
1083
- model = "gpt-5.1-codex" # Default
1075
+ model = "gpt-5.2-codex" # Default
1084
1076
  if hasattr(orchestrator, "lm") and hasattr(orchestrator.lm, "model"):
1085
1077
  model = orchestrator.lm.model
1086
1078
  elif hasattr(orchestrator, "config"):
@@ -0,0 +1,37 @@
1
+ """
2
+ Compression modules for infinite-running agents.
3
+
4
+ Two types of compression:
5
+ 1. TC (Tool Call) Compression - compresses tool call results before they enter context
6
+ 2. Rollout Compression - manages message history eviction (LRU-style)
7
+
8
+ These modules allow agents to run virtually indefinitely without context explosion.
9
+ """
10
+
11
+ from .tc_compression import (
12
+ TCCompressor,
13
+ NoOpTCCompressor,
14
+ NaiveSizeTCCompressor,
15
+ get_tc_compressor,
16
+ )
17
+ from .rollout_compression import (
18
+ RolloutCompressor,
19
+ NoOpRolloutCompressor,
20
+ LRURolloutCompressor,
21
+ SlidingWindowRolloutCompressor,
22
+ get_rollout_compressor,
23
+ )
24
+
25
+ __all__ = [
26
+ # TC Compression
27
+ "TCCompressor",
28
+ "NoOpTCCompressor",
29
+ "NaiveSizeTCCompressor",
30
+ "get_tc_compressor",
31
+ # Rollout Compression
32
+ "RolloutCompressor",
33
+ "NoOpRolloutCompressor",
34
+ "LRURolloutCompressor",
35
+ "SlidingWindowRolloutCompressor",
36
+ "get_rollout_compressor",
37
+ ]
@@ -0,0 +1,292 @@
1
+ """
2
+ Rollout Compression - manages message history eviction for infinite-running agents.
3
+
4
+ As agents run, their conversation history grows. These compressors implement
5
+ different strategies for evicting old messages to keep context bounded.
6
+
7
+ Available compressors:
8
+ - NoOpRolloutCompressor: No eviction (context will eventually overflow)
9
+ - LRURolloutCompressor: Evict oldest messages, keeping system prompt
10
+ - SlidingWindowRolloutCompressor: Keep last N turns (user+assistant pairs)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from abc import ABC, abstractmethod
16
+ from dataclasses import dataclass, field
17
+ from typing import Any
18
+
19
+
20
+ @dataclass
21
+ class EvictionStats:
22
+ """Statistics about message eviction."""
23
+
24
+ messages_before: int = 0
25
+ messages_after: int = 0
26
+ messages_evicted: int = 0
27
+ tokens_evicted_estimate: int = 0 # Rough estimate
28
+ eviction_triggered: bool = False
29
+
30
+ def to_dict(self) -> dict[str, Any]:
31
+ return {
32
+ "messages_before": self.messages_before,
33
+ "messages_after": self.messages_after,
34
+ "messages_evicted": self.messages_evicted,
35
+ "tokens_evicted_estimate": self.tokens_evicted_estimate,
36
+ "eviction_triggered": self.eviction_triggered,
37
+ }
38
+
39
+
40
+ class RolloutCompressor(ABC):
41
+ """
42
+ Abstract base class for rollout (message history) compression.
43
+
44
+ Subclasses implement different eviction strategies to keep the
45
+ conversation history bounded while preserving important context.
46
+ """
47
+
48
+ name: str = "base"
49
+
50
+ @abstractmethod
51
+ def compress(self, messages: list[dict]) -> tuple[list[dict], EvictionStats]:
52
+ """
53
+ Compress message history, returning trimmed version and stats.
54
+
55
+ Args:
56
+ messages: List of message dicts with 'role' and 'content' keys
57
+
58
+ Returns:
59
+ (compressed_messages, eviction_stats)
60
+ """
61
+ pass
62
+
63
+ def should_compress(self, messages: list[dict]) -> bool:
64
+ """Check if compression is needed (subclasses may override)."""
65
+ return True
66
+
67
+ def __repr__(self) -> str:
68
+ return f"{self.__class__.__name__}()"
69
+
70
+
71
+ class NoOpRolloutCompressor(RolloutCompressor):
72
+ """
73
+ No-op compressor - keeps all messages.
74
+
75
+ Use this when you want to disable rollout compression and let the
76
+ context window naturally overflow (will error eventually).
77
+ """
78
+
79
+ name = "noop"
80
+
81
+ def compress(self, messages: list[dict]) -> tuple[list[dict], EvictionStats]:
82
+ """Pass through unchanged."""
83
+ return messages, EvictionStats(
84
+ messages_before=len(messages),
85
+ messages_after=len(messages),
86
+ eviction_triggered=False,
87
+ )
88
+
89
+
90
+ class LRURolloutCompressor(RolloutCompressor):
91
+ """
92
+ LRU (Least Recently Used) compressor - evicts oldest messages.
93
+
94
+ Keeps the system prompt and the most recent messages. When the message
95
+ count exceeds max_messages, evicts oldest non-system messages.
96
+
97
+ Args:
98
+ max_messages: Maximum messages to keep (default: 50)
99
+ preserve_system: Keep all system messages (default: True)
100
+ preserve_first_user: Keep first user message as context (default: True)
101
+ """
102
+
103
+ name = "lru"
104
+
105
+ def __init__(
106
+ self,
107
+ max_messages: int = 50,
108
+ preserve_system: bool = True,
109
+ preserve_first_user: bool = True,
110
+ ):
111
+ self.max_messages = max_messages
112
+ self.preserve_system = preserve_system
113
+ self.preserve_first_user = preserve_first_user
114
+
115
+ def should_compress(self, messages: list[dict]) -> bool:
116
+ """Only compress if we exceed max_messages."""
117
+ return len(messages) > self.max_messages
118
+
119
+ def compress(self, messages: list[dict]) -> tuple[list[dict], EvictionStats]:
120
+ """Evict oldest messages, keeping system prompt and recent history."""
121
+ stats = EvictionStats(messages_before=len(messages))
122
+
123
+ if not self.should_compress(messages):
124
+ stats.messages_after = len(messages)
125
+ return messages, stats
126
+
127
+ # Separate preserved messages from evictable ones
128
+ preserved = []
129
+ evictable = []
130
+
131
+ first_user_seen = False
132
+ for i, msg in enumerate(messages):
133
+ role = msg.get("role", "")
134
+
135
+ # Always preserve system messages
136
+ if self.preserve_system and role == "system":
137
+ preserved.append((i, msg))
138
+ # Preserve first user message as task context
139
+ elif self.preserve_first_user and role == "user" and not first_user_seen:
140
+ preserved.append((i, msg))
141
+ first_user_seen = True
142
+ else:
143
+ evictable.append((i, msg))
144
+
145
+ # Calculate how many evictable messages to keep
146
+ preserved_count = len(preserved)
147
+ keep_count = max(0, self.max_messages - preserved_count)
148
+
149
+ # Keep the most recent evictable messages
150
+ kept_evictable = evictable[-keep_count:] if keep_count > 0 else []
151
+ evicted = evictable[:-keep_count] if keep_count > 0 and len(evictable) > keep_count else []
152
+
153
+ # Merge preserved and kept messages, maintaining original order
154
+ all_kept = preserved + kept_evictable
155
+ all_kept.sort(key=lambda x: x[0]) # Sort by original index
156
+ result = [msg for _, msg in all_kept]
157
+
158
+ # Estimate tokens evicted (rough: ~4 chars per token)
159
+ evicted_content = sum(len(str(msg.get("content", ""))) for _, msg in evicted)
160
+ tokens_evicted = evicted_content // 4
161
+
162
+ stats.messages_after = len(result)
163
+ stats.messages_evicted = len(evicted)
164
+ stats.tokens_evicted_estimate = tokens_evicted
165
+ stats.eviction_triggered = len(evicted) > 0
166
+
167
+ return result, stats
168
+
169
+ def __repr__(self) -> str:
170
+ return f"LRURolloutCompressor(max_messages={self.max_messages})"
171
+
172
+
173
+ class SlidingWindowRolloutCompressor(RolloutCompressor):
174
+ """
175
+ Sliding window compressor - keeps last N turns (user+assistant pairs).
176
+
177
+ A "turn" is a user message followed by an assistant response. This
178
+ preserves conversation coherence better than raw message count.
179
+
180
+ Args:
181
+ max_turns: Maximum turns to keep (default: 20)
182
+ preserve_system: Keep all system messages (default: True)
183
+ preserve_first_turn: Keep first turn as context (default: True)
184
+ """
185
+
186
+ name = "sliding_window"
187
+
188
+ def __init__(
189
+ self,
190
+ max_turns: int = 20,
191
+ preserve_system: bool = True,
192
+ preserve_first_turn: bool = True,
193
+ ):
194
+ self.max_turns = max_turns
195
+ self.preserve_system = preserve_system
196
+ self.preserve_first_turn = preserve_first_turn
197
+
198
+ def compress(self, messages: list[dict]) -> tuple[list[dict], EvictionStats]:
199
+ """Keep last N turns, preserving system messages."""
200
+ stats = EvictionStats(messages_before=len(messages))
201
+
202
+ # Extract system messages
203
+ system_messages = []
204
+ conversation = []
205
+
206
+ for msg in messages:
207
+ if msg.get("role") == "system":
208
+ system_messages.append(msg)
209
+ else:
210
+ conversation.append(msg)
211
+
212
+ # Group conversation into turns (user + assistant + tool results)
213
+ turns: list[list[dict]] = []
214
+ current_turn: list[dict] = []
215
+
216
+ for msg in conversation:
217
+ role = msg.get("role", "")
218
+ if role == "user" and current_turn:
219
+ # New user message starts a new turn
220
+ turns.append(current_turn)
221
+ current_turn = [msg]
222
+ else:
223
+ current_turn.append(msg)
224
+
225
+ # Don't forget the last turn
226
+ if current_turn:
227
+ turns.append(current_turn)
228
+
229
+ # Decide which turns to keep
230
+ if len(turns) <= self.max_turns:
231
+ # No eviction needed
232
+ result = system_messages + conversation
233
+ stats.messages_after = len(result)
234
+ return result, stats
235
+
236
+ # Keep first turn + last (max_turns - 1) turns
237
+ kept_turns = []
238
+ if self.preserve_first_turn and turns:
239
+ kept_turns.append(turns[0])
240
+ remaining_turns = turns[1:]
241
+ kept_turns.extend(remaining_turns[-(self.max_turns - 1):])
242
+ else:
243
+ kept_turns = turns[-self.max_turns:]
244
+
245
+ # Flatten kept turns back into messages
246
+ kept_conversation = []
247
+ for turn in kept_turns:
248
+ kept_conversation.extend(turn)
249
+
250
+ result = system_messages + kept_conversation
251
+
252
+ # Calculate eviction stats
253
+ evicted_count = len(messages) - len(result)
254
+ stats.messages_after = len(result)
255
+ stats.messages_evicted = evicted_count
256
+ stats.eviction_triggered = evicted_count > 0
257
+
258
+ return result, stats
259
+
260
+ def __repr__(self) -> str:
261
+ return f"SlidingWindowRolloutCompressor(max_turns={self.max_turns})"
262
+
263
+
264
+ # =============================================================================
265
+ # Factory
266
+ # =============================================================================
267
+
268
+
269
+ def get_rollout_compressor(
270
+ name: str = "lru",
271
+ **kwargs,
272
+ ) -> RolloutCompressor:
273
+ """
274
+ Get a rollout compressor by name.
275
+
276
+ Args:
277
+ name: Compressor name ("noop", "lru", "sliding_window")
278
+ **kwargs: Passed to compressor constructor
279
+
280
+ Returns:
281
+ Configured RolloutCompressor instance
282
+ """
283
+ compressors = {
284
+ "noop": NoOpRolloutCompressor,
285
+ "lru": LRURolloutCompressor,
286
+ "sliding_window": SlidingWindowRolloutCompressor,
287
+ }
288
+
289
+ if name not in compressors:
290
+ raise ValueError(f"Unknown rollout compressor: {name}. Available: {list(compressors.keys())}")
291
+
292
+ return compressors[name](**kwargs)
@@ -0,0 +1,165 @@
1
+ """
2
+ Tool Call (TC) Compression - compresses tool results before they enter context.
3
+
4
+ When an agent makes a tool call, the result can be arbitrarily large. These
5
+ compressors marshal results into a more digestible format for the agent.
6
+
7
+ Available compressors:
8
+ - NoOpTCCompressor: Pass-through, no compression (default for now)
9
+ - NaiveSizeTCCompressor: Truncate to last N characters
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from abc import ABC, abstractmethod
15
+ from typing import Any
16
+
17
+
18
+ class TCCompressor(ABC):
19
+ """
20
+ Abstract base class for tool call result compression.
21
+
22
+ Subclasses implement different compression strategies to prevent
23
+ tool results from exploding the agent's context window.
24
+ """
25
+
26
+ name: str = "base"
27
+
28
+ @abstractmethod
29
+ def compress(self, tool_name: str, result: Any) -> Any:
30
+ """
31
+ Compress a tool call result.
32
+
33
+ Args:
34
+ tool_name: Name of the tool that was called
35
+ result: The raw result from the tool
36
+
37
+ Returns:
38
+ Compressed result (same type or string)
39
+ """
40
+ pass
41
+
42
+ def __repr__(self) -> str:
43
+ return f"{self.__class__.__name__}()"
44
+
45
+
46
+ class NoOpTCCompressor(TCCompressor):
47
+ """
48
+ No-op compressor - passes results through unchanged.
49
+
50
+ Use this when tool results are already well-bounded or when you want
51
+ to disable compression entirely.
52
+ """
53
+
54
+ name = "noop"
55
+
56
+ def compress(self, tool_name: str, result: Any) -> Any:
57
+ """Pass through unchanged."""
58
+ return result
59
+
60
+
61
+ class NaiveSizeTCCompressor(TCCompressor):
62
+ """
63
+ Naive size-based compressor - truncates results to last N characters.
64
+
65
+ Simple but effective: keeps the most recent output which is usually
66
+ the most relevant (e.g., last N chars of a log file).
67
+
68
+ Args:
69
+ max_chars: Maximum characters to keep (default: 25000)
70
+ truncation_marker: String to prepend when truncated
71
+ """
72
+
73
+ name = "naive_size"
74
+
75
+ def __init__(
76
+ self,
77
+ max_chars: int = 25000,
78
+ truncation_marker: str = "... [truncated, showing last {n} chars] ...\n",
79
+ ):
80
+ self.max_chars = max_chars
81
+ self.truncation_marker = truncation_marker
82
+
83
+ def compress(self, tool_name: str, result: Any) -> Any:
84
+ """Truncate to last max_chars characters if needed."""
85
+ # Handle dict results (common for our tools)
86
+ if isinstance(result, dict):
87
+ return self._compress_dict(result)
88
+
89
+ # Handle string results
90
+ if isinstance(result, str):
91
+ return self._truncate_string(result)
92
+
93
+ # Handle list results
94
+ if isinstance(result, list):
95
+ return self._compress_list(result)
96
+
97
+ # For other types, convert to string and truncate
98
+ result_str = str(result)
99
+ return self._truncate_string(result_str)
100
+
101
+ def _truncate_string(self, s: str) -> str:
102
+ """Truncate string to last max_chars."""
103
+ if len(s) <= self.max_chars:
104
+ return s
105
+
106
+ # Keep last N chars with marker
107
+ marker = self.truncation_marker.format(n=self.max_chars)
108
+ keep_chars = self.max_chars - len(marker)
109
+ return marker + s[-keep_chars:]
110
+
111
+ def _compress_dict(self, d: dict) -> dict:
112
+ """Recursively compress dict values."""
113
+ compressed = {}
114
+ for key, value in d.items():
115
+ if isinstance(value, str):
116
+ compressed[key] = self._truncate_string(value)
117
+ elif isinstance(value, dict):
118
+ compressed[key] = self._compress_dict(value)
119
+ elif isinstance(value, list):
120
+ compressed[key] = self._compress_list(value)
121
+ else:
122
+ compressed[key] = value
123
+ return compressed
124
+
125
+ def _compress_list(self, lst: list) -> list:
126
+ """Compress list items."""
127
+ return [
128
+ self._truncate_string(item) if isinstance(item, str)
129
+ else self._compress_dict(item) if isinstance(item, dict)
130
+ else item
131
+ for item in lst
132
+ ]
133
+
134
+ def __repr__(self) -> str:
135
+ return f"NaiveSizeTCCompressor(max_chars={self.max_chars})"
136
+
137
+
138
+ # =============================================================================
139
+ # Factory
140
+ # =============================================================================
141
+
142
+
143
+ def get_tc_compressor(
144
+ name: str = "noop",
145
+ **kwargs,
146
+ ) -> TCCompressor:
147
+ """
148
+ Get a TC compressor by name.
149
+
150
+ Args:
151
+ name: Compressor name ("noop", "naive_size")
152
+ **kwargs: Passed to compressor constructor
153
+
154
+ Returns:
155
+ Configured TCCompressor instance
156
+ """
157
+ compressors = {
158
+ "noop": NoOpTCCompressor,
159
+ "naive_size": NaiveSizeTCCompressor,
160
+ }
161
+
162
+ if name not in compressors:
163
+ raise ValueError(f"Unknown TC compressor: {name}. Available: {list(compressors.keys())}")
164
+
165
+ return compressors[name](**kwargs)
zwarm/core/config.py CHANGED
@@ -40,9 +40,18 @@ class ExecutorConfig:
40
40
  # Note: web_search is always enabled via .codex/config.toml (set up by `zwarm init`)
41
41
 
42
42
 
43
+ @dataclass
44
+ class TCCompressionConfig:
45
+ """Configuration for tool call result compression."""
46
+
47
+ enabled: bool = True
48
+ compressor: str = "naive_size" # noop | naive_size
49
+ max_chars: int = 25000 # For naive_size compressor
50
+
51
+
43
52
  @dataclass
44
53
  class CompactionConfig:
45
- """Configuration for context window compaction."""
54
+ """Configuration for context window compaction (rollout compression)."""
46
55
 
47
56
  enabled: bool = True
48
57
  max_tokens: int = 100000 # Trigger compaction when estimated tokens exceed this
@@ -62,7 +71,10 @@ class OrchestratorConfig:
62
71
  max_steps: int = 50
63
72
  max_steps_per_turn: int = 60 # Max tool-call steps before returning to user (pilot mode)
64
73
  parallel_delegations: int = 4
65
- compaction: CompactionConfig = field(default_factory=CompactionConfig)
74
+
75
+ # Compression settings for infinite-running agents
76
+ compaction: CompactionConfig = field(default_factory=CompactionConfig) # Rollout compression
77
+ tc_compression: TCCompressionConfig = field(default_factory=TCCompressionConfig) # Tool call compression
66
78
 
67
79
  # Directory restrictions for agent delegations
68
80
  # None = only working_dir allowed (most restrictive, default)
@@ -115,10 +127,13 @@ class ZwarmConfig:
115
127
  orchestrator_data = data.get("orchestrator", {})
116
128
  watchers_data = data.get("watchers", {})
117
129
 
118
- # Parse compaction config from orchestrator
130
+ # Parse compression configs from orchestrator
119
131
  compaction_data = orchestrator_data.pop("compaction", {}) if orchestrator_data else {}
120
132
  compaction_config = CompactionConfig(**compaction_data) if compaction_data else CompactionConfig()
121
133
 
134
+ tc_compression_data = orchestrator_data.pop("tc_compression", {}) if orchestrator_data else {}
135
+ tc_compression_config = TCCompressionConfig(**tc_compression_data) if tc_compression_data else TCCompressionConfig()
136
+
122
137
  # Parse watchers config - handle both list shorthand and dict format
123
138
  if isinstance(watchers_data, list):
124
139
  # Shorthand: watchers: [progress, budget, scope]
@@ -140,11 +155,18 @@ class ZwarmConfig:
140
155
  message_role=watchers_data.get("message_role", "user"),
141
156
  )
142
157
 
143
- # Build orchestrator config with nested compaction
158
+ # Build orchestrator config with nested compression configs
144
159
  if orchestrator_data:
145
- orchestrator_config = OrchestratorConfig(**orchestrator_data, compaction=compaction_config)
160
+ orchestrator_config = OrchestratorConfig(
161
+ **orchestrator_data,
162
+ compaction=compaction_config,
163
+ tc_compression=tc_compression_config,
164
+ )
146
165
  else:
147
- orchestrator_config = OrchestratorConfig(compaction=compaction_config)
166
+ orchestrator_config = OrchestratorConfig(
167
+ compaction=compaction_config,
168
+ tc_compression=tc_compression_config,
169
+ )
148
170
 
149
171
  return cls(
150
172
  weave=WeaveConfig(**weave_data) if weave_data else WeaveConfig(),
@@ -183,6 +205,11 @@ class ZwarmConfig:
183
205
  "keep_first_n": self.orchestrator.compaction.keep_first_n,
184
206
  "keep_last_n": self.orchestrator.compaction.keep_last_n,
185
207
  },
208
+ "tc_compression": {
209
+ "enabled": self.orchestrator.tc_compression.enabled,
210
+ "compressor": self.orchestrator.tc_compression.compressor,
211
+ "max_chars": self.orchestrator.tc_compression.max_chars,
212
+ },
186
213
  },
187
214
  "watchers": {
188
215
  "enabled": self.watchers.enabled,