11// bot_loop.zig — Main poll → parse → dispatch → repeat loop
2+ // Phase 2: Two-thread arch — main thread polls, worker thread streams Claude output
23const std = @import ("std" );
34const telegram_api = @import ("telegram_api.zig" );
45const json_utils = @import ("json_utils.zig" );
56const command_parser = @import ("command_parser.zig" );
67const handlers = @import ("handlers.zig" );
8+ const claude_stream = @import ("claude_stream.zig" );
79
810const BotConfig = telegram_api .BotConfig ;
911
12+ /// Shared state for streaming — module-level, accessible from main + worker threads
13+ var stream_state = claude_stream.StreamState {};
14+
1015/// Run the bot loop: poll Telegram, parse commands, dispatch handlers.
11- /// Never returns (infinite loop).
16+ /// Never returns (infinite loop). Main thread stays responsive while
17+ /// worker thread handles Claude streaming.
1218pub fn run (allocator : std.mem.Allocator , config : BotConfig ) void {
1319 var last_update_id : i64 = 0 ;
1420
1521 // Announce startup
16- telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xf0\x9f\xa4\x96 TRI BOT online! Send /help for commands." );
22+ telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xf0\x9f\xa4\x96 TRI BOT v2.0 online! Send /help for commands." );
1723
18- std .debug .print ("[tri-bot] Started. Polling Telegram...\n " , .{});
24+ std .debug .print ("[tri-bot] Started (Phase 2: streaming) . Polling Telegram...\n " , .{});
1925
2026 while (true ) {
2127 const body = telegram_api .getUpdates (allocator , config .bot_token , last_update_id + 1 ) orelse {
@@ -25,48 +31,31 @@ pub fn run(allocator: std.mem.Allocator, config: BotConfig) void {
2531 };
2632 defer allocator .free (body );
2733
28- // Process each update
29- const Context = struct {
30- allocator : std.mem.Allocator ,
31- config : BotConfig ,
32- max_id : i64 ,
33- };
34- var ctx = Context {
35- .allocator = allocator ,
36- .config = config ,
37- .max_id = last_update_id ,
38- };
39-
40- // We can't use closures in Zig, so use a global-style dispatch.
41- // Instead, manually iterate updates with a simple loop.
42- processUpdates (allocator , config , body , & ctx .max_id );
43- last_update_id = ctx .max_id ;
34+ var max_id = last_update_id ;
35+ processUpdates (allocator , config , body , & max_id );
36+ last_update_id = max_id ;
4437 }
4538}
4639
4740fn processUpdates (allocator : std.mem.Allocator , config : BotConfig , body : []const u8 , max_id : * i64 ) void {
48- // Find each "update_id": block manually
4941 var pos : usize = 0 ;
5042 while (pos < body .len ) {
5143 const needle = "\" update_id\" :" ;
5244 const idx = std .mem .indexOfPos (u8 , body , pos , needle ) orelse break ;
5345
54- // Determine block boundary (next update_id or end)
5546 const next_idx = std .mem .indexOfPos (u8 , body , idx + needle .len + 1 , needle ) orelse body .len ;
5647 const block = body [idx .. next_idx ];
5748
58- // Extract update_id
5949 const uid = json_utils .extractInt (block , "update_id" ) orelse {
6050 pos = idx + needle .len ;
6151 continue ;
6252 };
6353
64- // Update max
6554 if (uid > max_id .* ) {
6655 max_id .* = uid ;
6756 }
6857
69- // Extract chat_id
58+ // Extract chat_id from nested message.chat.id
7059 const chat_id_val = blk : {
7160 const chat_needle = "\" chat\" :{\" id\" :" ;
7261 const ci = std .mem .indexOf (u8 , block , chat_needle ) orelse break :blk @as (i64 , 0 );
@@ -76,26 +65,22 @@ fn processUpdates(allocator: std.mem.Allocator, config: BotConfig, body: []const
7665 break :blk std .fmt .parseInt (i64 , block [cs .. ce ], 10 ) catch 0 ;
7766 };
7867
79- // Auth check: only respond to configured chat_id
68+ // Auth check
8069 const expected_chat_id = std .fmt .parseInt (i64 , config .chat_id , 10 ) catch 0 ;
8170 if (chat_id_val != expected_chat_id ) {
82- std .debug .print ("[tri-bot] Ignoring update from chat {d} (expected {d}) \n " , .{ chat_id_val , expected_chat_id });
71+ std .debug .print ("[tri-bot] Ignoring update from chat {d}\n " , .{chat_id_val });
8372 pos = next_idx ;
8473 continue ;
8574 }
8675
87- // Extract text
8876 const text = json_utils .extractString (block , "text" ) orelse {
8977 pos = next_idx ;
9078 continue ;
9179 };
9280
9381 std .debug .print ("[tri-bot] Update {d}: \" {s}\" \n " , .{ uid , text });
9482
95- // Parse command
9683 const cmd = command_parser .parse (text );
97-
98- // Dispatch
9984 dispatch (allocator , config , cmd );
10085
10186 pos = next_idx ;
@@ -106,15 +91,45 @@ fn dispatch(allocator: std.mem.Allocator, config: BotConfig, cmd: command_parser
10691 if (std .mem .eql (u8 , cmd .name , "help" )) {
10792 handlers .handleHelp (allocator , config );
10893 } else if (std .mem .eql (u8 , cmd .name , "ask" )) {
109- handlers .handleAsk (allocator , config , cmd .args );
94+ // Phase 2: streaming /ask via worker thread
95+ if (cmd .args .len == 0 ) {
96+ telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x9a\xa0 Usage: /ask <question>" );
97+ return ;
98+ }
99+ if (stream_state .is_busy .load (.acquire )) {
100+ telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x8f\xb3 Already processing. Send /stop first." );
101+ return ;
102+ }
103+ // Dupe args — they point into getUpdates body which will be freed
104+ const args_owned = allocator .dupe (u8 , cmd .args ) catch return ;
105+ stream_state .is_busy .store (true , .release );
106+ _ = std .Thread .spawn (.{}, claude_stream .runStreaming , .{ allocator , config , args_owned , false , & stream_state }) catch {
107+ stream_state .is_busy .store (false , .release );
108+ allocator .free (args_owned );
109+ telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x9d\x8c Failed to spawn worker thread" );
110+ return ;
111+ };
110112 } else if (std .mem .eql (u8 , cmd .name , "continue" )) {
111- handlers .handleContinue (allocator , config , cmd .args );
113+ // Phase 2: streaming /continue via worker thread
114+ if (stream_state .is_busy .load (.acquire )) {
115+ telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x8f\xb3 Already processing. Send /stop first." );
116+ return ;
117+ }
118+ const args_owned = allocator .dupe (u8 , cmd .args ) catch return ;
119+ stream_state .is_busy .store (true , .release );
120+ _ = std .Thread .spawn (.{}, claude_stream .runStreaming , .{ allocator , config , args_owned , true , & stream_state }) catch {
121+ stream_state .is_busy .store (false , .release );
122+ allocator .free (args_owned );
123+ telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x9d\x8c Failed to spawn worker thread" );
124+ return ;
125+ };
112126 } else if (std .mem .eql (u8 , cmd .name , "status" )) {
127+ // Status stays blocking (short query, no streaming needed)
113128 handlers .handleStatus (allocator , config );
114129 } else if (std .mem .eql (u8 , cmd .name , "stop" )) {
115- telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x9b\x94 /stop not yet implemented (Phase 2)" );
130+ // Phase 2: /stop kills active Claude process
131+ claude_stream .stopProcess (allocator , config , & stream_state );
116132 } else if (cmd .name .len > 0 ) {
117- // Unknown command
118133 var buf : [256 ]u8 = undefined ;
119134 telegram_api .sendFmt (allocator , config .bot_token , config .chat_id , & buf , "\xe2\x9d\x93 Unknown command: /{s}. Try /help" , .{cmd .name });
120135 }
0 commit comments