brainiac-discord 0.0.8 → 0.0.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffe7aa25d1f9a232e356f5dc8781f96c7ec35527de9505650102260f0384664a
4
- data.tar.gz: 0e4cb055a833dd4eeff966e215632266786b275b1d02135ba6e7afe9cfb016bb
3
+ metadata.gz: ed2e8f74f05207e45944b37b43094298419d007ed897dac9987f97d86a03152b
4
+ data.tar.gz: 9ac14bdbb6e5481d7b36a8734f8b4beb66eb7b8299caa1459e47c6389f186101
5
5
  SHA512:
6
- metadata.gz: 3e4747823b694d2e96492090616339d58b6f388b6812df0abd0c88741bbaae1e1e5aba4118ed600d9cb8e7e750ca13669b6d3af4d2d249ad89ab6118bf7e4df2
7
- data.tar.gz: 0df896adfa8ae958664f49ae3f54e0020c1ead26d42e657226e3ddefb9d4797b31fef357c45ccf7de278a181ac316dda559d82c29eff2a8a8959560b961bb5f1
6
+ metadata.gz: c74ff0656c3790f82604c8162c826758b5ca57ea0eecb71adc5e7fcf7d78ee6eccebb110dffada6a6ba4e74456c75c98fdcee88679cbd2716a52d73f53231cf6
7
+ data.tar.gz: 9c5ffd03b96f76668d2cbfcfb54bebd93af6ca2501f7d952d635a9db75f2cd712d3e40274e23dee877fc835ac0a305264eb27b5383411e4b35288477a236e038
@@ -79,7 +79,7 @@ module Brainiac
79
79
  agent_key: agent_key, agent_name: agent_name, bot_token: bot_token, is_bot: is_bot,
80
80
  channel_id: channel_id, message_id: message_id, message: message,
81
81
  clean_content: clean_content, clean_content_for_prompt: tags[:clean_text],
82
- chat_mode: tags[:chat_mode], is_thread: is_thread, is_dm: is_dm,
82
+ chat_mode: tags[:chat_mode], fresh: tags[:fresh], is_thread: is_thread, is_dm: is_dm,
83
83
  channel_info: channel_info, parent_channel_id: parent_channel_id,
84
84
  discord_user: discord_user, reply_context: reply_context,
85
85
  channel_history: channel_history, project_key: project_key,
@@ -389,7 +389,7 @@ module Brainiac
389
389
  end
390
390
 
391
391
  def route_dispatch(agent_key:, agent_name:, bot_token:, is_bot:, channel_id:, message_id:, message:,
392
- clean_content:, clean_content_for_prompt:, chat_mode:, is_thread:, is_dm:,
392
+ clean_content:, clean_content_for_prompt:, chat_mode:, fresh: false, is_thread:, is_dm:,
393
393
  channel_info:, parent_channel_id:, discord_user:, reply_context:,
394
394
  channel_history:, project_key:, project_config:, attachment_paths:,
395
395
  directly_addressed: false)
@@ -421,7 +421,7 @@ module Brainiac
421
421
  agent_key: agent_key, agent_name: agent_name, bot_token: bot_token,
422
422
  channel_id: channel_id, message_id: message_id, message: message,
423
423
  clean_content: clean_content, clean_content_for_prompt: clean_content_for_prompt,
424
- chat_mode: chat_mode, is_thread: is_thread, is_dm: is_dm,
424
+ chat_mode: chat_mode, fresh: fresh, is_thread: is_thread, is_dm: is_dm,
425
425
  channel_info: channel_info, parent_channel_id: parent_channel_id,
426
426
  discord_user: discord_user, reply_context: reply_context,
427
427
  channel_history: channel_history, project_key: project_key,
@@ -484,7 +484,7 @@ module Brainiac
484
484
  end
485
485
 
486
486
  def dispatch_session(agent_key:, agent_name:, bot_token:, channel_id:, message_id:, message:,
487
- clean_content:, clean_content_for_prompt:, chat_mode:, is_thread:, is_dm:,
487
+ clean_content:, clean_content_for_prompt:, chat_mode:, fresh: false, is_thread:, is_dm:,
488
488
  channel_info:, parent_channel_id:, discord_user:, reply_context:,
489
489
  channel_history:, project_key:, project_config:, project_context:,
490
490
  session_key:, supersede_key:, attachment_paths:, is_bot:)
@@ -504,6 +504,24 @@ module Brainiac
504
504
  chat_mode: chat_mode, bot_token: bot_token
505
505
  )
506
506
 
507
+ # [fresh] tag forces a new session — skip resume even if viable
508
+ if fresh && should_resume
509
+ should_resume = false
510
+ LOG.info "[Discord:#{agent_name}] [fresh] tag — forcing new session instead of resuming" if defined?(LOG)
511
+ end
512
+
513
+ # [fresh] memory refresh: before starting the new session, spawn a quick agent
514
+ # to review the full thread history and update memory
515
+ if fresh && is_thread
516
+ refresh_memory_from_thread(
517
+ agent_key: agent_key, agent_name: agent_name, bot_token: bot_token,
518
+ channel_id: channel_id, message_id: message_id, card_id: card_id,
519
+ project_config: project_config, cli_provider_override: thread_cli_provider,
520
+ work_dir: thread_worktree_path || (project_config ? project_config["repo_path"] : Dir.pwd),
521
+ timestamp: timestamp, response_dir: response_dir
522
+ )
523
+ end
524
+
507
525
  prompt = build_prompt(
508
526
  should_resume: should_resume, thread_worktree_path: thread_worktree_path,
509
527
  clean_content_for_prompt: clean_content_for_prompt,
@@ -884,6 +902,136 @@ module Brainiac
884
902
  pk == "brainiac" ? capture_git_state(work_dir) : [nil, nil]
885
903
  end
886
904
 
905
+ # Fetch ALL messages in a thread (paginated), returning formatted lines.
906
+ def fetch_full_thread_history(channel_id, before_message_id, token:)
907
+ all_messages = []
908
+ cursor = before_message_id
909
+ limit = 100
910
+
911
+ loop do
912
+ url = "/channels/#{channel_id}/messages?before=#{cursor}&limit=#{limit}"
913
+ batch = Api.request(:get, url, token: token)
914
+ break unless batch.is_a?(Array) && batch.any?
915
+
916
+ all_messages.concat(batch)
917
+ break if batch.size < limit
918
+
919
+ cursor = batch.last["id"]
920
+ end
921
+
922
+ return "" if all_messages.empty?
923
+
924
+ lines = all_messages.reverse.filter_map do |msg|
925
+ author = msg.dig("author", "username") || "unknown"
926
+ content = msg["content"]&.strip || ""
927
+ next if content.empty?
928
+
929
+ "#{author}: #{content}"
930
+ end
931
+
932
+ lines.join("\n")
933
+ rescue StandardError => e
934
+ LOG.warn "[Discord] Failed to fetch full thread history: #{e.message}" if defined?(LOG)
935
+ ""
936
+ end
937
+
938
+ # Spawn a synchronous agent session to review full thread history and update memory.
939
+ # This runs BEFORE the main [fresh] dispatch so the new session gets up-to-date memory.
940
+ def refresh_memory_from_thread(agent_key:, agent_name:, bot_token:, channel_id:, message_id:,
941
+ card_id:, project_config:, cli_provider_override:, work_dir:,
942
+ timestamp:, response_dir:)
943
+ Api.add_reaction(channel_id, message_id, "📖", token: bot_token)
944
+ LOG.info "[Discord:#{agent_name}] [fresh] Refreshing memory from full thread history" if defined?(LOG)
945
+
946
+ full_history = fetch_full_thread_history(channel_id, message_id, token: bot_token)
947
+ if full_history.empty?
948
+ LOG.info "[Discord:#{agent_name}] [fresh] No thread history to review — skipping memory refresh" if defined?(LOG)
949
+ Api.remove_reaction(channel_id, message_id, "📖", token: bot_token)
950
+ Api.add_reaction(channel_id, message_id, "🔄", token: bot_token)
951
+ return
952
+ end
953
+
954
+ agent_config_name = agent_key.downcase.gsub(/[^a-z0-9-]/, "-")
955
+ memory_dir = memory_dir_for(agent_name)
956
+ memory_file = File.join(memory_dir, "card-#{card_id}.md")
957
+
958
+ prompt = build_memory_refresh_prompt(
959
+ agent_name: agent_name, memory_file: memory_file, full_history: full_history, card_id: card_id
960
+ )
961
+
962
+ prompt_file = File.join(response_dir, "discord-memory-refresh-#{timestamp}-#{agent_key}-#{message_id}.md")
963
+ File.write(prompt_file, prompt)
964
+
965
+ resolved = resolve_project_cli_config(project_config || DEFAULT_PROJECT,
966
+ cli_provider_override: cli_provider_override, agent_name: agent_name)
967
+ # Use a fast model for memory refresh (sonnet or haiku) — we just need it to read and summarize
968
+ refresh_model = (resolved["allowed_models"] || {})["sonnet"] || "claude-sonnet-4.6"
969
+ cmd = build_agent_cmd(resolved, agent_config_name: agent_config_name, model: refresh_model,
970
+ effort: "low", prompt_file: prompt_file, resume: false)
971
+
972
+ log_file = File.join(response_dir, "discord-memory-refresh-#{timestamp}-#{agent_key}-#{message_id}.log")
973
+ spawn_env = agent_env_for(agent_name)
974
+ prompt_mode = resolved["prompt_mode"] || "stdin"
975
+
976
+ LOG.info "[Discord:#{agent_name}] [fresh] Spawning memory refresh (model: #{refresh_model}), log: #{log_file}" if defined?(LOG)
977
+
978
+ pid = spawn(spawn_env, *cmd,
979
+ chdir: work_dir,
980
+ **(prompt_mode == "stdin" ? { in: prompt_file } : {}),
981
+ out: [log_file, "w"],
982
+ err: %i[child out])
983
+
984
+ # Wait synchronously — the main dispatch doesn't start until memory is updated
985
+ Process.wait(pid)
986
+ exit_status = $CHILD_STATUS.exitstatus
987
+
988
+ if exit_status == 0
989
+ LOG.info "[Discord:#{agent_name}] [fresh] Memory refresh completed successfully" if defined?(LOG)
990
+ else
991
+ LOG.warn "[Discord:#{agent_name}] [fresh] Memory refresh failed (exit: #{exit_status}), proceeding anyway" if defined?(LOG)
992
+ end
993
+
994
+ Api.remove_reaction(channel_id, message_id, "📖", token: bot_token)
995
+ Api.add_reaction(channel_id, message_id, "🔄", token: bot_token)
996
+ rescue StandardError => e
997
+ LOG.error "[Discord:#{agent_name}] [fresh] Memory refresh error: #{e.message}" if defined?(LOG)
998
+ Api.remove_reaction(channel_id, message_id, "📖", token: bot_token)
999
+ Api.add_reaction(channel_id, message_id, "🔄", token: bot_token)
1000
+ end
1001
+
1002
+ # Build the prompt for the memory-refresh agent. This agent reads the full thread
1003
+ # and updates the memory file so the fresh session starts with accurate context.
1004
+ def build_memory_refresh_prompt(agent_name:, memory_file:, full_history:, card_id:)
1005
+ <<~PROMPT
1006
+ You are #{agent_name}. Your ONLY job in this session is to update your memory file.
1007
+
1008
+ ## Instructions
1009
+ 1. Read your current memory file at `#{memory_file}` (it may be empty or outdated)
1010
+ 2. Review the COMPLETE thread conversation below
1011
+ 3. Rewrite the memory file with an accurate, comprehensive summary that captures:
1012
+ - The original topic/question that started the thread
1013
+ - ALL major topics discussed, decisions made, and direction changes
1014
+ - Current status of any implementation work
1015
+ - Key file paths, branch names, PR URLs mentioned
1016
+ - Open questions or next steps
1017
+ - Any user preferences or corrections expressed
1018
+ 4. Write the updated content to `#{memory_file}`
1019
+
1020
+ ## Rules
1021
+ - Do NOT produce any other output or write any other files
1022
+ - Do NOT make code changes
1023
+ - Do NOT create branches or commits
1024
+ - Focus purely on creating an accurate memory summary
1025
+ - Keep it concise but complete — future sessions depend on this
1026
+ - Preserve any information from the existing memory that's still relevant
1027
+
1028
+ ## Complete Thread History (oldest first)
1029
+ ```
1030
+ #{full_history}
1031
+ ```
1032
+ PROMPT
1033
+ end
1034
+
887
1035
  def monitor_agent(pid:, session_key:, agent_name:, agent_config_name:, channel_id:, message_id:,
888
1036
  bot_token:, response_file:, meta_file:, prompt_file:, log_file:,
889
1037
  attachment_paths:, project_config:, head_before:, status_before:)
@@ -927,6 +1075,13 @@ module Brainiac
927
1075
  LOG.info "[Discord:#{agent_name}] Agent finished for message #{message_id} (exit: #{exit_status.exitstatus})" if defined?(LOG)
928
1076
 
929
1077
  if exit_status.exitstatus && exit_status.exitstatus != 0 && !File.exist?(response_file)
1078
+ if transient_cli_error?(log_file)
1079
+ LOG.warn "[Discord:#{agent_name}] Transient CLI error for message #{message_id} — reacting instead of crash notify" if defined?(LOG)
1080
+ Api.remove_reaction(channel_id, message_id, "👀", token: bot_token)
1081
+ Api.add_reaction(channel_id, message_id, "⚡", token: bot_token)
1082
+ return
1083
+ end
1084
+
930
1085
  notify_agent_crash(
931
1086
  exit_status: exit_status.exitstatus, log_file: log_file,
932
1087
  agent_name: agent_name, source: :discord,
@@ -1014,6 +1169,30 @@ module Brainiac
1014
1169
  end
1015
1170
  end
1016
1171
 
1172
+ # Detect transient CLI errors that don't warrant a full crash notification.
1173
+ # These are random upstream failures (model timeouts, tool approval glitches)
1174
+ # that resolve on retry — reacting with an emoji is sufficient.
1175
+ TRANSIENT_CLI_ERROR_PATTERN = /
1176
+ Failed\sto\sreceive\sthe\snext\smessage|
1177
+ Kiro\sfailed\sto\sgenerate\sa\sresponse|
1178
+ Tool\sapproval\srequired\sbut\s--no-interactive|
1179
+ Kiro\sis\shaving\strouble\sresponding
1180
+ /ix
1181
+
1182
+ def transient_cli_error?(log_file)
1183
+ return false unless log_file && File.exist?(log_file)
1184
+
1185
+ # Read last 4KB to avoid loading huge logs — transient errors appear at the end
1186
+ File.open(log_file, "rb") do |f|
1187
+ size = f.size
1188
+ f.seek([size - 4096, 0].max)
1189
+ tail = f.read
1190
+ tail.match?(TRANSIENT_CLI_ERROR_PATTERN)
1191
+ end
1192
+ rescue StandardError
1193
+ false
1194
+ end
1195
+
1017
1196
  def log_post_task_crash_diagnostics(log_file, agent_name)
1018
1197
  return unless log_file && File.exist?(log_file)
1019
1198
 
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Discord
6
- VERSION = "0.0.8"
6
+ VERSION = "0.0.9"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac-discord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis