Skip to content

Commit 167fe58

Browse files
committed
feat(config): make hardcoded values configurable via environment variables
Add three new configuration options that can be overridden by environment variables: - OPENCODER_MAX_FILE_SIZE: Maximum file size for reading files (default: 1MB) - OPENCODER_LOG_BUFFER_SIZE: Buffer size for logger formatting (default: 2KB) - OPENCODER_TASK_PAUSE_SECONDS: Pause duration between tasks (default: 2s) Changes: - Add max_file_size, log_buffer_size, task_pause_seconds to Config struct - Update Config.loadFromEnv() to parse new environment variables - Update fsutil.readFile() to accept max_size parameter - Update Logger to use dynamic buffer allocation based on buffer_size - Thread config values through all modules that need them - Update all tests to include new config fields This allows users to tune opencoder behavior without code changes. Signed-off-by: leocavalcante <[email protected]>
1 parent 72f1da3 commit 167fe58

File tree

9 files changed

+97
-31
lines changed

9 files changed

+97
-31
lines changed

src/config.zig

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ pub const Config = struct {
7676
max_retries: u32,
7777
backoff_base: u32,
7878
log_retention: u32,
79+
max_file_size: usize,
80+
log_buffer_size: usize,
81+
task_pause_seconds: u64,
7982

8083
/// Default configuration values
8184
pub const defaults = Config{
@@ -87,6 +90,9 @@ pub const Config = struct {
8790
.max_retries = 3,
8891
.backoff_base = 10,
8992
.log_retention = 30,
93+
.max_file_size = 1024 * 1024, // 1MB
94+
.log_buffer_size = 2048, // 2KB
95+
.task_pause_seconds = 2, // 2 seconds
9096
};
9197

9298
/// Load configuration from environment variables
@@ -108,6 +114,21 @@ pub const Config = struct {
108114
config.log_retention = std.fmt.parseInt(u32, val, 10) catch defaults.log_retention;
109115
}
110116

117+
// OPENCODER_MAX_FILE_SIZE (in bytes)
118+
if (std.posix.getenv("OPENCODER_MAX_FILE_SIZE")) |val| {
119+
config.max_file_size = std.fmt.parseInt(usize, val, 10) catch defaults.max_file_size;
120+
}
121+
122+
// OPENCODER_LOG_BUFFER_SIZE (in bytes)
123+
if (std.posix.getenv("OPENCODER_LOG_BUFFER_SIZE")) |val| {
124+
config.log_buffer_size = std.fmt.parseInt(usize, val, 10) catch defaults.log_buffer_size;
125+
}
126+
127+
// OPENCODER_TASK_PAUSE_SECONDS
128+
if (std.posix.getenv("OPENCODER_TASK_PAUSE_SECONDS")) |val| {
129+
config.task_pause_seconds = std.fmt.parseInt(u64, val, 10) catch defaults.task_pause_seconds;
130+
}
131+
111132
return config;
112133
}
113134
};
@@ -164,6 +185,9 @@ test "Config.defaults has expected values" {
164185
try std.testing.expectEqual(@as(u32, 3), config.max_retries);
165186
try std.testing.expectEqual(@as(u32, 10), config.backoff_base);
166187
try std.testing.expectEqual(@as(u32, 30), config.log_retention);
188+
try std.testing.expectEqual(@as(usize, 1024 * 1024), config.max_file_size);
189+
try std.testing.expectEqual(@as(usize, 2048), config.log_buffer_size);
190+
try std.testing.expectEqual(@as(u64, 2), config.task_pause_seconds);
167191
try std.testing.expectEqual(false, config.verbose);
168192
try std.testing.expectEqual(@as(?[]const u8, null), config.user_hint);
169193
}

src/evaluator.zig

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ pub fn evaluate(
2525
plan_path: []const u8,
2626
cycle: u32,
2727
allocator: Allocator,
28+
max_size: usize,
2829
) !EvaluationResult {
2930
// First check if all tasks are marked complete
3031
const pending_tasks = blk: {
31-
const content = fsutil.readFile(plan_path, allocator) catch |err| {
32+
const content = fsutil.readFile(plan_path, allocator, max_size) catch |err| {
3233
if (err == error.FileNotFound) {
3334
// No plan file, needs work (planning)
3435
return .needs_work;
@@ -54,8 +55,8 @@ pub fn evaluate(
5455
}
5556

5657
/// Quick check if plan has pending tasks without AI evaluation
57-
pub fn hasPendingTasks(plan_path: []const u8, allocator: Allocator) bool {
58-
const content = fsutil.readFile(plan_path, allocator) catch {
58+
pub fn hasPendingTasks(plan_path: []const u8, allocator: Allocator, max_size: usize) bool {
59+
const content = fsutil.readFile(plan_path, allocator, max_size) catch {
5960
return false;
6061
};
6162
defer allocator.free(content);

src/executor.zig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ fn createTestLogger(allocator: Allocator) !*Logger {
271271
.cycle = 0,
272272
.verbose = false,
273273
.allocator = allocator,
274+
.buffer_size = 2048,
274275
};
275276
return logger_ptr;
276277
}
@@ -299,6 +300,9 @@ test "Executor.init creates executor" {
299300
.max_retries = 3,
300301
.backoff_base = 1,
301302
.log_retention = 30,
303+
.max_file_size = 1024 * 1024,
304+
.log_buffer_size = 2048,
305+
.task_pause_seconds = 2,
302306
};
303307

304308
const executor = Executor.init(&test_cfg, test_logger, allocator);
@@ -320,6 +324,9 @@ test "Executor.initWithCmd creates executor with custom command" {
320324
.max_retries = 3,
321325
.backoff_base = 1,
322326
.log_retention = 30,
327+
.max_file_size = 1024 * 1024,
328+
.log_buffer_size = 2048,
329+
.task_pause_seconds = 2,
323330
};
324331

325332
const executor = Executor.initWithCmd(&test_cfg, test_logger, allocator, "./test_helpers/mock_opencode.sh");
@@ -345,6 +352,9 @@ test "runOpencode handles successful execution" {
345352
.max_retries = 3,
346353
.backoff_base = 1,
347354
.log_retention = 30,
355+
.max_file_size = 1024 * 1024,
356+
.log_buffer_size = 2048,
357+
.task_pause_seconds = 2,
348358
};
349359

350360
// Get absolute path to mock script
@@ -377,6 +387,9 @@ test "runOpencode handles process failure" {
377387
.max_retries = 3,
378388
.backoff_base = 1,
379389
.log_retention = 30,
390+
.max_file_size = 1024 * 1024,
391+
.log_buffer_size = 2048,
392+
.task_pause_seconds = 2,
380393
};
381394

382395
// Get absolute path to mock script
@@ -406,6 +419,9 @@ test "runOpencode passes session ID when continuing" {
406419
.max_retries = 3,
407420
.backoff_base = 1,
408421
.log_retention = 30,
422+
.max_file_size = 1024 * 1024,
423+
.log_buffer_size = 2048,
424+
.task_pause_seconds = 2,
409425
};
410426

411427
// Get absolute path to mock script

src/fs.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ pub fn fileExists(path: []const u8) bool {
9191
}
9292

9393
/// Read entire file contents
94-
pub fn readFile(path: []const u8, allocator: Allocator) ![]u8 {
94+
pub fn readFile(path: []const u8, allocator: Allocator, max_size: usize) ![]u8 {
9595
const file = try fs.cwd().openFile(path, .{});
9696
defer file.close();
97-
return try file.readToEndAlloc(allocator, 1024 * 1024); // 1MB max
97+
return try file.readToEndAlloc(allocator, max_size);
9898
}
9999

100100
/// Write contents to file
@@ -168,7 +168,7 @@ test "readFile and writeFile work correctly" {
168168
try writeFile(test_file, content);
169169

170170
// Read file
171-
const read_content = try readFile(test_file, allocator);
171+
const read_content = try readFile(test_file, allocator, 1024 * 1024);
172172
defer allocator.free(read_content);
173173

174174
try std.testing.expectEqualStrings(content, read_content);

src/logger.zig

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ pub const Logger = struct {
1717
cycle: u32,
1818
verbose: bool,
1919
allocator: Allocator,
20+
buffer_size: usize,
2021

2122
/// Initialize logger with opencoder directory
2223
pub fn init(
2324
opencoder_dir: []const u8,
2425
verbose: bool,
2526
allocator: Allocator,
27+
buffer_size: usize,
2628
) !Logger {
2729
// Open main log file
2830
const main_log_path = try std.fs.path.join(allocator, &.{ opencoder_dir, "logs", "main.log" });
@@ -47,6 +49,7 @@ pub const Logger = struct {
4749
.cycle = 1,
4850
.verbose = verbose,
4951
.allocator = allocator,
52+
.buffer_size = buffer_size,
5053
};
5154
}
5255

@@ -71,8 +74,9 @@ pub const Logger = struct {
7174

7275
/// Log message with formatting
7376
pub fn logFmt(self: *Logger, comptime fmt: []const u8, args: anytype) void {
74-
var buf: [2048]u8 = undefined;
75-
const msg = std.fmt.bufPrint(&buf, fmt, args) catch return;
77+
const buf = self.allocator.alloc(u8, self.buffer_size) catch return;
78+
defer self.allocator.free(buf);
79+
const msg = std.fmt.bufPrint(buf, fmt, args) catch return;
7680
self.log(msg);
7781
}
7882

@@ -86,8 +90,9 @@ pub const Logger = struct {
8690

8791
/// Say with formatting
8892
pub fn sayFmt(self: *Logger, comptime fmt: []const u8, args: anytype) void {
89-
var buf: [2048]u8 = undefined;
90-
const msg = std.fmt.bufPrint(&buf, fmt, args) catch return;
93+
const buf = self.allocator.alloc(u8, self.buffer_size) catch return;
94+
defer self.allocator.free(buf);
95+
const msg = std.fmt.bufPrint(buf, fmt, args) catch return;
9196
self.say(msg);
9297
}
9398

@@ -100,8 +105,9 @@ pub const Logger = struct {
100105

101106
/// Status with formatting
102107
pub fn statusFmt(self: *Logger, comptime fmt: []const u8, args: anytype) void {
103-
var buf: [2048]u8 = undefined;
104-
const msg = std.fmt.bufPrint(&buf, fmt, args) catch return;
108+
const buf = self.allocator.alloc(u8, self.buffer_size) catch return;
109+
defer self.allocator.free(buf);
110+
const msg = std.fmt.bufPrint(buf, fmt, args) catch return;
105111
self.status(msg);
106112
}
107113

@@ -113,16 +119,18 @@ pub const Logger = struct {
113119

114120
/// Log error with formatting
115121
pub fn logErrorFmt(self: *Logger, comptime fmt: []const u8, args: anytype) void {
116-
var buf: [2048]u8 = undefined;
117-
const msg = std.fmt.bufPrint(&buf, fmt, args) catch return;
122+
const buf = self.allocator.alloc(u8, self.buffer_size) catch return;
123+
defer self.allocator.free(buf);
124+
const msg = std.fmt.bufPrint(buf, fmt, args) catch return;
118125
self.logError(msg);
119126
}
120127

121128
/// Log verbose (only if verbose mode enabled)
122129
pub fn logVerbose(self: *Logger, msg: []const u8) void {
123130
if (self.verbose) {
124-
var buf: [2048]u8 = undefined;
125-
const verbose_msg = std.fmt.bufPrint(&buf, "VERBOSE: {s}", .{msg}) catch return;
131+
const buf = self.allocator.alloc(u8, self.buffer_size) catch return;
132+
defer self.allocator.free(buf);
133+
const verbose_msg = std.fmt.bufPrint(buf, "VERBOSE: {s}", .{msg}) catch return;
126134
self.say(verbose_msg);
127135
}
128136
}

src/loop.zig

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ pub const Loop = struct {
8989
}
9090

9191
// Validate plan content
92-
const plan_content = fsutil.readFile(self.paths.current_plan, self.allocator) catch {
92+
const plan_content = fsutil.readFile(self.paths.current_plan, self.allocator, self.cfg.max_file_size) catch {
9393
self.log.logError("Failed to read plan file");
9494
self.backoffSleep();
9595
continue;
@@ -115,7 +115,7 @@ pub const Loop = struct {
115115
self.log.logFmt("[Cycle {d}] Executing tasks...", .{self.st.cycle});
116116

117117
while (!shutdown_requested) {
118-
const plan_content = fsutil.readFile(self.paths.current_plan, self.allocator) catch {
118+
const plan_content = fsutil.readFile(self.paths.current_plan, self.allocator, self.cfg.max_file_size) catch {
119119
self.log.logError("Failed to read plan file");
120120
break;
121121
};
@@ -162,14 +162,14 @@ pub const Loop = struct {
162162
}
163163

164164
// Mark task complete regardless of result to not get stuck
165-
plan.markTaskComplete(self.paths.current_plan, task.line_number, self.allocator) catch {
165+
plan.markTaskComplete(self.paths.current_plan, task.line_number, self.allocator, self.cfg.max_file_size) catch {
166166
self.log.logError("Failed to mark task complete");
167167
};
168168

169169
try self.st.save(self.paths.state_file, self.allocator);
170170

171171
// Small pause between tasks
172-
std.Thread.sleep(2 * std.time.ns_per_s);
172+
std.Thread.sleep(self.cfg.task_pause_seconds * std.time.ns_per_s);
173173
}
174174

175175
// Evaluation phase
@@ -178,6 +178,7 @@ pub const Loop = struct {
178178
self.paths.current_plan,
179179
self.st.cycle,
180180
self.allocator,
181+
self.cfg.max_file_size,
181182
) catch |err| {
182183
self.log.logErrorFmt("Evaluation error: {}", .{err});
183184
// Default to needs_work on error
@@ -206,7 +207,7 @@ pub const Loop = struct {
206207
self.executor.resetSession();
207208
} else {
208209
// Check if there are actually pending tasks
209-
if (!evaluator.hasPendingTasks(self.paths.current_plan, self.allocator)) {
210+
if (!evaluator.hasPendingTasks(self.paths.current_plan, self.allocator, self.cfg.max_file_size)) {
210211
self.log.sayFmt("[Cycle {d}] NEEDS_WORK but no tasks, starting new cycle", .{self.st.cycle});
211212

212213
// Archive and start fresh
@@ -294,6 +295,7 @@ fn createTestLogger(allocator: Allocator) !*Logger {
294295
.cycle = 0,
295296
.verbose = false,
296297
.allocator = allocator,
298+
.buffer_size = 2048,
297299
};
298300
return logger_ptr;
299301
}
@@ -370,6 +372,9 @@ test "Loop.init creates loop with correct fields" {
370372
.max_retries = 3,
371373
.backoff_base = 5,
372374
.log_retention = 30,
375+
.max_file_size = 1024 * 1024,
376+
.log_buffer_size = 2048,
377+
.task_pause_seconds = 2,
373378
};
374379

375380
var test_state = state.State.default();
@@ -415,6 +420,9 @@ test "backoffSleep calculates correct sleep time" {
415420
.max_retries = 3,
416421
.backoff_base = 10,
417422
.log_retention = 30,
423+
.max_file_size = 1024 * 1024,
424+
.log_buffer_size = 2048,
425+
.task_pause_seconds = 2,
418426
};
419427

420428
var test_state = state.State.default();
@@ -465,6 +473,9 @@ test "Loop state transitions between phases" {
465473
.max_retries = 3,
466474
.backoff_base = 1,
467475
.log_retention = 30,
476+
.max_file_size = 1024 * 1024,
477+
.log_buffer_size = 2048,
478+
.task_pause_seconds = 2,
468479
};
469480

470481
var test_state = state.State.default();
@@ -519,6 +530,9 @@ test "Loop handles task counter increments" {
519530
.max_retries = 3,
520531
.backoff_base = 1,
521532
.log_retention = 30,
533+
.max_file_size = 1024 * 1024,
534+
.log_buffer_size = 2048,
535+
.task_pause_seconds = 2,
522536
};
523537

524538
var test_state = state.State.default();
@@ -571,6 +585,9 @@ test "Loop cycle reset on new cycle" {
571585
.max_retries = 3,
572586
.backoff_base = 1,
573587
.log_retention = 30,
588+
.max_file_size = 1024 * 1024,
589+
.log_buffer_size = 2048,
590+
.task_pause_seconds = 2,
574591
};
575592

576593
var test_state = state.State{

src/main.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ fn runOpencoder(cfg: config.Config, allocator: std.mem.Allocator) !void {
6060
defer paths.deinit();
6161

6262
// Initialize logger
63-
var log = try Logger.init(paths.opencoder_dir, cfg.verbose, allocator);
63+
var log = try Logger.init(paths.opencoder_dir, cfg.verbose, allocator, cfg.log_buffer_size);
6464
defer log.deinit();
6565

6666
log.say("Workspace initialized");
6767

6868
// Load or create state
6969
var st = blk: {
70-
if (try state.State.load(paths.state_file, allocator)) |loaded| {
70+
if (try state.State.load(paths.state_file, allocator, cfg.max_file_size)) |loaded| {
7171
log.sayFmt("Resuming: Cycle {d}, Phase {s}", .{
7272
loaded.cycle,
7373
loaded.phase.toString(),
@@ -81,7 +81,7 @@ fn runOpencoder(cfg: config.Config, allocator: std.mem.Allocator) !void {
8181

8282
// Recalculate task counts if resuming with existing plan
8383
if (fsutil.fileExists(paths.current_plan)) {
84-
const plan_content = fsutil.readFile(paths.current_plan, allocator) catch null;
84+
const plan_content = fsutil.readFile(paths.current_plan, allocator, cfg.max_file_size) catch null;
8585
if (plan_content) |content| {
8686
defer allocator.free(content);
8787
const plan_mod = @import("plan.zig");

src/plan.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ pub fn countTotalTasks(plan_content: []const u8) u32 {
9494
}
9595

9696
/// Mark a task as complete at the given line number
97-
pub fn markTaskComplete(path: []const u8, line_number: usize, allocator: Allocator) !void {
97+
pub fn markTaskComplete(path: []const u8, line_number: usize, allocator: Allocator, max_size: usize) !void {
9898
// Read file
99-
const content = try fsutil.readFile(path, allocator);
99+
const content = try fsutil.readFile(path, allocator, max_size);
100100
defer allocator.free(content);
101101

102102
// Build new content with task marked complete

0 commit comments

Comments
 (0)