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 +4 -4
- data/lib/brainiac/plugins/discord/message.rb +183 -4
- data/lib/brainiac/plugins/discord/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed2e8f74f05207e45944b37b43094298419d007ed897dac9987f97d86a03152b
|
|
4
|
+
data.tar.gz: 9ac14bdbb6e5481d7b36a8734f8b4beb66eb7b8299caa1459e47c6389f186101
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|