11// bot_loop.zig — Main poll → parse → dispatch → repeat loop
2- // Phase 2: Two-thread arch — main thread polls, worker thread streams Claude output
2+ // Phase 2.5 : Two-thread arch + session management (/model, /resume, /sessions)
33const std = @import ("std" );
44const telegram_api = @import ("telegram_api.zig" );
55const json_utils = @import ("json_utils.zig" );
@@ -12,16 +12,19 @@ const BotConfig = telegram_api.BotConfig;
1212/// Shared state for streaming — module-level, accessible from main + worker threads
1313var stream_state = claude_stream.StreamState {};
1414
15+ /// Runtime state — model selection, persists across commands within a bot run
16+ var bot_state = handlers.BotState {};
17+
1518/// Run the bot loop: poll Telegram, parse commands, dispatch handlers.
1619/// Never returns (infinite loop). Main thread stays responsive while
1720/// worker thread handles Claude streaming.
1821pub fn run (allocator : std.mem.Allocator , config : BotConfig ) void {
1922 var last_update_id : i64 = 0 ;
2023
2124 // Announce startup
22- telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xf0\x9f\xa4\x96 TRI BOT v2 .0 online! Send /help for commands." );
25+ telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xf0\x9f\xa4\x96 TRI BOT v3 .0 online! Send /help for commands." );
2326
24- std .debug .print ("[tri-bot] Started (Phase 2: streaming). Polling Telegram...\n " , .{});
27+ std .debug .print ("[tri-bot] Started (Phase 2.5 : streaming + sessions ). Polling Telegram...\n " , .{});
2528
2629 while (true ) {
2730 const body = telegram_api .getUpdates (allocator , config .bot_token , last_update_id + 1 ) orelse {
@@ -87,11 +90,23 @@ fn processUpdates(allocator: std.mem.Allocator, config: BotConfig, body: []const
8790 }
8891}
8992
93+ /// Helper: check if busy + spawn streaming worker thread.
94+ fn spawnStreaming (allocator : std.mem.Allocator , config : BotConfig , opts : claude_stream.StreamOpts ) void {
95+ stream_state .is_busy .store (true , .release );
96+ _ = std .Thread .spawn (.{}, claude_stream .runStreaming , .{ allocator , config , opts , & stream_state }) catch {
97+ stream_state .is_busy .store (false , .release );
98+ allocator .free (opts .args );
99+ if (opts .resume_id ) | rid | allocator .free (rid );
100+ telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x9d\x8c Failed to spawn worker thread" );
101+ return ;
102+ };
103+ }
104+
90105fn dispatch (allocator : std.mem.Allocator , config : BotConfig , cmd : command_parser.Command ) void {
91106 if (std .mem .eql (u8 , cmd .name , "help" )) {
92107 handlers .handleHelp (allocator , config );
93108 } else if (std .mem .eql (u8 , cmd .name , "ask" )) {
94- // Phase 2: streaming /ask via worker thread
109+ // Streaming /ask via worker thread
95110 if (cmd .args .len == 0 ) {
96111 telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x9a\xa0 Usage: /ask <question>" );
97112 return ;
@@ -100,34 +115,58 @@ fn dispatch(allocator: std.mem.Allocator, config: BotConfig, cmd: command_parser
100115 telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x8f\xb3 Already processing. Send /stop first." );
101116 return ;
102117 }
103- // Dupe args — they point into getUpdates body which will be freed
104118 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- };
119+ spawnStreaming (allocator , config , .{
120+ .args = args_owned ,
121+ .model = bot_state .getModel (),
122+ });
112123 } else if (std .mem .eql (u8 , cmd .name , "continue" )) {
113- // Phase 2: streaming /continue via worker thread
124+ // Streaming /continue via worker thread
114125 if (stream_state .is_busy .load (.acquire )) {
115126 telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x8f\xb3 Already processing. Send /stop first." );
116127 return ;
117128 }
118129 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" );
130+ spawnStreaming (allocator , config , .{
131+ .args = args_owned ,
132+ .use_continue = true ,
133+ .model = bot_state .getModel (),
134+ });
135+ } else if (std .mem .eql (u8 , cmd .name , "resume" )) {
136+ // Streaming /resume <id> via worker thread
137+ if (cmd .args .len == 0 ) {
138+ telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x9a\xa0 Usage: /resume <session-id>" );
139+ return ;
140+ }
141+ if (stream_state .is_busy .load (.acquire )) {
142+ telegram_api .sendMessage (allocator , config .bot_token , config .chat_id , "\xe2\x8f\xb3 Already processing. Send /stop first." );
143+ return ;
144+ }
145+ // Extract session ID (first word of args)
146+ const id_end = std .mem .indexOf (u8 , cmd .args , " " ) orelse cmd .args .len ;
147+ const resume_id = allocator .dupe (u8 , cmd .args [0.. id_end ]) catch return ;
148+ // Remaining text after ID is the prompt (if any)
149+ const prompt_start = if (id_end < cmd .args .len ) id_end + 1 else id_end ;
150+ const prompt = allocator .dupe (u8 , cmd .args [prompt_start .. ]) catch {
151+ allocator .free (resume_id );
124152 return ;
125153 };
154+ spawnStreaming (allocator , config , .{
155+ .args = prompt ,
156+ .resume_id = resume_id ,
157+ .model = bot_state .getModel (),
158+ });
159+ } else if (std .mem .eql (u8 , cmd .name , "model" )) {
160+ // Blocking: set/show model
161+ handlers .handleModel (allocator , config , cmd .args , & bot_state );
162+ } else if (std .mem .eql (u8 , cmd .name , "sessions" )) {
163+ // Blocking: list sessions
164+ handlers .handleSessions (allocator , config );
126165 } else if (std .mem .eql (u8 , cmd .name , "status" )) {
127- // Status stays blocking (short query, no streaming needed)
166+ // Blocking: project status
128167 handlers .handleStatus (allocator , config );
129168 } else if (std .mem .eql (u8 , cmd .name , "stop" )) {
130- // Phase 2: /stop kills active Claude process
169+ // Kill active Claude process
131170 claude_stream .stopProcess (allocator , config , & stream_state );
132171 } else if (cmd .name .len > 0 ) {
133172 var buf : [256 ]u8 = undefined ;
0 commit comments