11// agent_loop.zig — Sleep-wake cycle for Ralph autonomous agent
2+ // Hooks handle per-tool Telegram reporting. This loop only sends WAKE/SLEEP.
3+ // Uses --continue for native session resume (replaces HANDOVER.md).
24const std = @import ("std" );
35const identity_mod = @import ("identity.zig" );
46const handover = @import ("handover.zig" );
57const github_poller = @import ("github_poller.zig" );
68const context_builder = @import ("context_builder.zig" );
79const claude_runner = @import ("claude_runner.zig" );
810const state_mod = @import ("state.zig" );
11+ const telegram = @import ("telegram.zig" );
912
1013pub const Config = struct {
1114 project_root : []const u8 ,
@@ -15,7 +18,8 @@ pub const Config = struct {
1518 sleep_interval_s : u64 = 1800 , // 30 minutes
1619 max_turns : u32 = 50 ,
1720 max_wakes : u32 = 0 , // 0 = infinite
18- single_shot : bool = false , // true = run once and exit
21+ single_shot : bool = false ,
22+ tg_config : telegram.TelegramConfig = .{ .bot_token = "" , .chat_id = "" , .enabled = false },
1923};
2024
2125fn log (comptime fmt : []const u8 , args : anytype ) void {
@@ -32,22 +36,28 @@ pub fn run(allocator: std.mem.Allocator, config: Config) !void {
3236 log (" repo: {s}/{s}" , .{ config .owner , config .repo });
3337 log (" sleep interval: {d}s" , .{config .sleep_interval_s });
3438 log (" max turns: {d}" , .{config .max_turns });
39+ log (" telegram: {s}" , .{if (config .tg_config .enabled ) "enabled" else "disabled" });
3540
3641 while (true ) {
3742 // === WAKE ===
3843 const wake_count = state .incrementWakeCount () catch 0 ;
3944 log ("=== WAKE #{d} ===" , .{wake_count });
4045
46+ // Telegram: announce wake
47+ var tg_buf : [512 ]u8 = undefined ;
48+ telegram .sendFmt (config .tg_config , & tg_buf , "<b>ralph</b> | WAKE #{d}" , .{wake_count });
49+
4150 if (config .max_wakes > 0 and wake_count > config .max_wakes ) {
4251 log ("Max wakes ({d}) reached. Exiting." , .{config .max_wakes });
52+ telegram .send (config .tg_config , "<b>ralph</b> | Max wakes reached. Stopping." );
4353 break ;
4454 }
4555
4656 // Load identity
4757 var id = identity_mod .load (allocator , config .project_root );
4858 defer id .deinit ();
4959
50- // Read previous handover
60+ // Read previous handover (used for first wake context only)
5161 const handover_content = handover .read (allocator , config .project_root );
5262 defer if (handover_content ) | h | allocator .free (h );
5363
@@ -63,11 +73,15 @@ pub fn run(allocator: std.mem.Allocator, config: Config) !void {
6373
6474 if (issues_json == null ) {
6575 log ("No pending issues or GitHub API unavailable. Sleeping." , .{});
76+ telegram .send (config .tg_config , "<b>ralph</b> | No issues found. Sleeping." );
6677 if (config .single_shot ) break ;
6778 std .Thread .sleep (config .sleep_interval_s * std .time .ns_per_s );
6879 continue ;
6980 }
7081
82+ // Telegram: issues found
83+ telegram .sendFmt (config .tg_config , & tg_buf , "<b>ralph</b> | Issues found, building context..." , .{});
84+
7185 // Read current state
7286 const current_issue = state .read ("current_issue" );
7387 defer if (current_issue ) | v | allocator .free (v );
@@ -87,14 +101,25 @@ pub fn run(allocator: std.mem.Allocator, config: Config) !void {
87101
88102 log ("Context built ({d} bytes). Spawning Claude CLI..." , .{prompt .len });
89103
104+ // Use --continue for session resume after first wake
105+ const use_continue = wake_count > 1 ;
106+
107+ // Telegram: spawning Claude
108+ telegram .sendFmt (config .tg_config , & tg_buf , "<b>ralph</b> | Spawning Claude ({d}b context, {s})..." , .{
109+ prompt .len ,
110+ if (use_continue ) "--continue" else "new session" ,
111+ });
112+
90113 // === WORK ===
91114 var result = claude_runner .spawn (
92115 allocator ,
93116 prompt ,
94117 config .project_root ,
95118 config .max_turns ,
119+ use_continue ,
96120 ) catch | err | {
97121 log ("Claude spawn error: {s}" , .{@errorName (err )});
122+ telegram .sendFmt (config .tg_config , & tg_buf , "<b>ralph</b> | Claude spawn FAILED: {s}" , .{@errorName (err )});
98123 if (config .single_shot ) break ;
99124 std .Thread .sleep (config .sleep_interval_s * std .time .ns_per_s );
100125 continue ;
@@ -103,33 +128,30 @@ pub fn run(allocator: std.mem.Allocator, config: Config) !void {
103128
104129 log ("Claude exited with code {d} ({d} bytes output)" , .{ result .exit_code , result .stdout .len });
105130
131+ // Telegram: Claude finished
132+ if (result .exit_code == 0 ) {
133+ telegram .sendFmt (config .tg_config , & tg_buf , "<b>ralph</b> | Claude done ({d}b output)" , .{result .stdout .len });
134+ } else {
135+ telegram .sendFmt (config .tg_config , & tg_buf , "<b>ralph</b> | Claude exit={d} ({d}b output)" , .{ result .exit_code , result .stdout .len });
136+ }
137+
106138 // Save session log
107139 claude_runner .saveLog (allocator , config .project_root , result .stdout );
108140
109141 // === SLEEP ===
110- // Check if handover was written by the session
111- const new_handover = handover .read (allocator , config .project_root );
112- if (new_handover ) | nh | {
113- allocator .free (nh );
114- } else {
115- // Emergency handover — session didn't write one
116- log ("WARNING: No handover written. Creating emergency handover." , .{});
117- handover .writeEmergency (allocator , config .project_root , wake_count , current_issue ) catch {
118- log ("Failed to write emergency handover!" , .{});
119- };
120- }
121-
122142 // Update state
123143 var count_buf : [16 ]u8 = undefined ;
124144 const count_str = std .fmt .bufPrint (& count_buf , "{d}" , .{wake_count }) catch "0" ;
125145 state .write ("last_wake" , count_str ) catch {};
126146
127147 if (config .single_shot ) {
128148 log ("Single-shot mode. Exiting." , .{});
149+ telegram .send (config .tg_config , "<b>ralph</b> | Single-shot done. Exiting." );
129150 break ;
130151 }
131152
132153 log ("Sleeping for {d}s..." , .{config .sleep_interval_s });
154+ telegram .sendFmt (config .tg_config , & tg_buf , "<b>ralph</b> | Sleeping {d}s..." , .{config .sleep_interval_s });
133155 std .Thread .sleep (config .sleep_interval_s * std .time .ns_per_s );
134156 }
135157
0 commit comments