Commit Graph

138 Commits

Author SHA1 Message Date
gitea ed8b271b4e Gate librarian cog on a full ping round-trip, not a bare GET
CI / compile (pull_request) Successful in 9s
CI / unit (pull_request) Successful in 18s
CI / integration (pull_request) Successful in 18s
The librarian health check was a plain GET to '/', which only proved
Flask was listening - not that the service could actually take a query,
run it through its internal queue+worker, and answer back. So the cog
could load against a librarian whose worker was wedged or that couldn't
reach the bot on the return leg.

Replace it with a ping that travels the SAME path a real search does, on
both sides:
  bot: QueryControl -> OUT_COMM_Q -> scan_queue -> awaiting_q
  librarian: POST /ping -> librarian_queue -> worker pulls it off
             (no Crossref/DOI search) -> pongs back with the same uuid
  bot: /conjurer -> incoming_q -> scan_incoming matches uuid, wakes waiter
The cog enables only when that whole loop closes within 3s. This also
proves the librarian->bot return path, which a GET never did.

Safety: uuid is random per ping; the wait and POST are both bounded so
startup can't stall; a pong that finds no waiter is dropped (never
orphaned into IN_COMM_Q, which would make the cog post a bogus 'no
results' message); and a ping whose pong never returns is swept out of
awaiting_q after PING_TTL_SECONDS so nothing leaks. All awaiting_q writes
stay within scan_queue (append) and scan_incoming (remove) - no locks,
no cross-thread mutation.

Integration tests cover: OK round-trip, timeout when accepted-but-no-pong,
unreachable, non-200, orphan-pong-dropped, and that real results still
reach IN_COMM_Q. Suite: 24 integration + 41 unit green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 15:00:04 +02:00
gitea 442b8a2a60 Make service health-check self-diagnosing
build / build (push) Failing after 9s
CI / compile (push) Successful in 9s
CI / unit (push) Successful in 19s
CI / integration (push) Successful in 14s
When a service group's cog stays disabled the log now says WHY:
_service_health returns the actual connection error (ConnectionError
= refused/down, ConnectTimeout = firewall/slow, gaierror = DNS/wrong
host) instead of a bare 'unreachable', and on_ready logs the resolved
FILE/LIBRARIAN/RADIO addresses so an env-var that never reached the
process (address falls back to the 192.168.1.15:5000 default) is
obvious at a glance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 11:52:04 +00:00
gitea e1fca864d8 oracle: $runy / $runa_dnia - Elder Futhark rune readings in-character
CI / compile (pull_request) Successful in 10s
CI / unit (pull_request) Successful in 14s
CI / integration (pull_request) Successful in 10s
build / build (push) Successful in 38s
CI / compile (push) Successful in 10s
CI / unit (push) Successful in 17s
CI / integration (push) Successful in 11s
Fits the mythology pillar of the persona (Slavic/Norse/Celtic, Old Norse
phrases). New always-loaded cog oracle_commands:

* $runy [pytanie] draws three Elder Futhark runes (past/present/future, with
  upright/reversed orientation - the 8 symmetric runes are never reversed) and
  asks the ACTIVE AI backend to read the spread in Conjurer's voice. If the AI
  is down it still shows the drawn runes with their own meanings, so the command
  always answers.
* $runa_dnia gives one rune, deterministic per user per day (sha256 seed), so
  it's stable if asked repeatedly - no AI call, no state file.

The full 24-rune Futhark, the draw logic and the reversal rules are pure and
unit-tested (distinct draw, non-invertible never reversed, per-day stability,
meaning fallback).

Also fixes a pre-existing unit-job breakage: test_bar_commands and
test_lore_commands each stubbed `discord` with different completeness and
shared sys.modules, so once both landed on main the one lacking `discord.ext.tasks`
shadowed the one needing it and collection failed order-dependently. A new
tests/unit/conftest.py stubs discord once, completely, before any test module -
the per-file stubs then skip. Full unit job: 41 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 20:38:48 +02:00
gitea 3f4a1d5083 lore: bound pamiec.json by summarising old memory into "Legendy Baru"
CI / compile (pull_request) Successful in 8s
CI / unit (pull_request) Failing after 10s
CI / integration (pull_request) Successful in 12s
build / build (push) Successful in 37s
CI / compile (push) Successful in 38s
CI / unit (push) Failing after 1m19s
CI / integration (push) Successful in 9s
The conversation memory file grows forever (every chat appends a user+assistant
pair), so startup load gets slower and the disk fills. New always-loaded cog
lore_commands turns that growth into content: a background task summarises the
oldest slice into one in-character "legend" via the ACTIVE AI backend, replaces
those old messages with the summary (bounding the file, keeping continuity for
the next startup's context), archives it to legendy.json, and announces it on

Safety: the compaction transforms (build_transcript, apply_compaction) are pure
and unit-tested. The file rewrite is re-read -> back up -> atomic write with no
await in between, so a handle_response append that lands while the summary is
being generated can neither be lost (it's in the preserved tail) nor corrupt
the file (single-threaded, no interleave). A .bak is kept. Scope note: this
bounds the on-disk file (startup/disk); the in-RAM MESSAGE_TABLE is a separate
concern left untouched to avoid yanking context from a live conversation.

Commands: $zapisz_legende (Vykidailo) forces a compaction now; $legendy recalls
a random past legend. All thresholds env-overridable (CONJURER_MEMORY_COMPACT_*,
CONJURER_LEGENDS_CHANNEL). constants gains LEGENDS_FILE + config + seed; bot.py
registers the cog.

Verified: tests/unit/test_lore_commands.py covers prefix-replace/tail-keep,
preservation of appends made during summarisation, and transcript formatting +
head/tail truncation. Unit job 32 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 20:12:59 +02:00
gitea e9e731e2bd bar: $nalej invents cocktails, $menu keeps the bar's growing lore
CI / compile (pull_request) Successful in 10s
CI / unit (pull_request) Successful in 16s
CI / integration (pull_request) Successful in 11s
build / build (push) Successful in 42s
CI / compile (push) Successful in 9s
CI / unit (push) Successful in 14s
CI / integration (push) Successful in 11s
The most in-character capability the bot has: the persona is literally a 200kg
bartender who mixes strong drinks with intriguing names. New always-loaded cog
bar_commands:

* $nalej [motyw] asks the ACTIVE AI backend (whatever $gadaj_teraz selects) to
  invent one themed cocktail in Conjurer's voice - persona reused from
  GPT_SETTINGS[0] as a system message, instructions as the user turn, via
  handle_response request_type NONE so it never pollutes the bar's conversation
  memory. Empty motyw = a surprise; "radio"/"pod muzykę" themes the drink on the
  track currently playing (PREPPED_TRACKS["now_playing"]).
* every drink is appended to menu.json (new seeded state file, CONJURER_MENU_FILE
  overridable) with name/theme/author/timestamp/full text - emergent bar lore.
* $menu lists the invented drinks and pours one at random from the archive.

Text-only for now; a DALL-E drink image is an easy follow-up (the render path
already exists in ai_commands, OpenAI-only).

constants gains MENU_FILE (next to pamiec.json by default) + its seed; bot.py
registers bar_commands as a core cog. Verified: tests/unit/test_bar_commands.py
covers name extraction (markers/markdown/fallback) and the menu round-trip
incl. corrupt-file tolerance; unit job 32 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 16:55:20 +02:00
Michal Tuszowski 2a21fc9e8c split: betoniarka (radio operator) colocated with liquidsoap; musician goes Discord-only
Permissions post-mortem that motivated this: the musician wrote radio
playlists AS ROOT onto a ROOT-OWNED network share which liquidsoap then
read AS USER 'radio' - chown fails on such shares by design (root squash /
uid mapping), so the radio came up and died on the playlists. The fix is
structural: the playlist WRITER now lives in the same container as the
READER, as the same user, on a local volume. No shared partition, no
chown, no uid mapping.

New: conjurer_betoniarka/betoniarka.py - runs inside the radio container
(started by the entrypoint as user 'radio', port 5005):
- library scan -> all_playlist/hit playlists with LOCAL container paths
  (start + every 24h + authenticated GET /rescan)
- bot-facing radio API moved from the musician: /add_to_priority,
  /create_priority_playlist, /request_radio_file, /clear_pr_pls,
  plus GET /ping (health) and /stream (web page)
- radio_log/persistence tailer forwarding play events to the bot's
  /prepped_tracks with the shared API key (bot-unreachable = logged, not fatal)

Musician: pure Discord music player now - keeps /mp3, /update_mp3,
/get_music and the file-share endpoints; all radio playlist writing, radio
paths/env and the tailer removed.

Bot: new CONJURER_RADIO_SERVICE (defaults to CONJURER_FILE_SERVICE so
un-split deployments keep working); radio_commands targets it; separate
'radio' health-gate group on betoniarka /ping (musician group now covers
music_commands + file_search_commands only).

Docker: betoniarka baked into the radio image (python3 + flask/waitress/
requests from Debian debs), port 5005 exposed, entrypoint starts it via
setpriv as 'radio'; data-volume chown is now best-effort with a loud
warning (keep the volume local); docs get the post-mortem + wiring.

Verified: py_compile everything; functional stub tests - rescan writes
local-path playlists, wyszukaj scores and appends to priority, auth
401/ok, tailer forward carries the API key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:42:41 +02:00
Michal Tuszowski cd28c6d4db fix: Client.start() takes no log_handler kwarg (run()-only)
log_handler belongs to Client.run() (which configures logging and then
calls start()); passing it to start() raised TypeError at launch. This was
inherited from the never-run dockerised variant's thin_client.py. We
configure our own handlers, so simply drop the kwarg.

Also set logger.propagate=False: a dependency calls logging.basicConfig(),
so every 'discord' record was printed twice (our format + root's).

Verified with a strict-signature stub client (start(token, *, reconnect)).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 01:07:18 +02:00
Michal Tuszowski 460cf072ad fix: never swallow task exceptions - diagnose WHY the bot died
The restart-loop with only 'Starting discord bot' in the logs was caused by
gather(return_exceptions=True) eating the client.start() exception: a failed
login looked like a clean exit and docker just restarted the container.

bot.py:
- _run_bot: catch discord.LoginFailure / PrivilegedIntentsRequired with
  CRITICAL messages telling exactly what to fix (token entry / Developer
  Portal intents), full traceback for anything else, then re-raise
- main() returns an exit code; every task exception is re-logged as the
  LAST line in docker logs ('Task died: ...'); process exits 1 on failure
- log the token source (env vs netrc) and its length at startup to catch
  truncated/wrong-field netrc entries
- _run_comm_subroutine: comm crash is logged loudly but no longer relies on
  gather to surface it (bot keeps running - Discord side is independent)

communication_subroutine.py (crash found by the new stub tests):
- comm_subroutine used 'logger' before its local assignment
  (UnboundLocalError killed the whole comm layer at every startup)
- re-comment the flask_debug thread: it binds the same host:port as
  waitress, so running both dies with 'address in use'

Verified via stub-injected client: LoginFailure -> CRITICAL + exit 1,
PrivilegedIntentsRequired -> CRITICAL + exit 1, unknown exception -> full
traceback + exit 1, clean shutdown -> exit 0; comm layer no longer crashes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 01:07:18 +02:00
Michal Tuszowski 020a6b114a fix: make bot startup resilient (self-healing state, gated cogs, loud failures)
Container/native startup died silently on any missing file/service. Now:

constants.py - self-healing runtime layout at import:
- create missing dirs (log dir, LOGSTORE, GRAPHICS_PATH, MUSIC_FOLDER)
- seed missing state files from the repo templates shipped next to
  constants.py (settings/system_gpt/pamiec/pamiec_muzyki/accident_log),
  falling back to safe empty JSON; existing files are NEVER overwritten

bot.py:
- log to stdout too, so 'docker logs' finally shows the crash reason
- missing Discord token = loud sys.exit with mount/env instructions
  (was: silent return -> container crash-loop with empty logs)
- every cog loads independently (one broken cog = skipped with traceback,
  bot continues)
- musician/librarian cogs are health-gated: enabled only when the service
  answers HTTP; a watchdog re-checks every 5 min and enables them the
  moment the service comes alive (no restart needed); tree re-synced
- on_ready reconnects no longer re-load extensions

requirements_conan.txt + Dockerfile.bot: aiomcrcon (Python <=3.11 only)
moved to best-effort extras so the 3.13 image builds clean and the
conanjurer cog stays dormant without it.

DOCKER_PROXMOX.md: startup model (core vs gated cogs) + crash-loop
troubleshooting incl. the 'disappearing files' checklist (nothing in the
stack deletes host files; bind mount = live state).

Verified: fresh-volume seeding creates dirs+templates, existing files
untouched, missing-token exits with FATAL message, health-gating logic
(stub-based runpy tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 01:07:18 +02:00
gitea 759c04a48f add 2026-07-07 23:24:12 +02:00
Michal Tuszowski 5adeb1b384 Land prototype on main (fix stacked-PR retarget gap)
PRs #10 and #11 were merged into their intermediate base branches
(restructure/working-copy-root and proto-improvements) rather than main,
because the stacked PRs' bases were not auto-retargeted (the branches were
not deleted on merge). As a result main only received the #9 restructure
and is still the plain working-copy bot.

This brings the full prototype onto main as a clean delta on top of the
current main tree (identical content to proto-improvements, but with main
ancestry so it merges without the squash-induced rename/delete conflicts):

- constants.py: env-var config, safe JSON loading, dependency guards,
  env->netrc tokens, API_SHARED_KEY + service_headers(), CONAN_* config
- communication_subroutine.py: queue timeout/Empty, daemon threads,
  cooperative stop_event, inbound _authorize_request()
- bot.py: asyncio event loop + load conanjurer_commands
- music_functions / radio_commands / librarian_commands: X-Conjurer-Api-Key
- conanjurer_commands/_functions: fixed + integrated bridge with RCON
  player-join notifications
- requirements_bot.txt: aiomcrcon, asyncssh
- conjurer_musician/.gitignore: keep runtime playlists/mp3 out of the repo

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:28:14 +02:00
Michal Tuszowski a64fb2da57 Restructure: promote working_copy to repo root
Make the stable 'working copy' bot the canonical code at the repository
root so the install/deploy scripts operate against it again.

- Move working_copy/* to root (bot entrypoint is bot.py)
- Restore root-level install/ops scripts from c4fa88e (deploy.sh,
  install_main_bot.sh, status_report.*, conjurer.service, etc.)
- Fix deploy.sh: copy bot.py (was thin_client.py) and add the
  conanjurer_* modules; bump command count
- Remove side-by-side variant dirs (backup_old_docker, prototype_one,
  prototype_musician_one, musician_old, working_copy) and docker cruft
- Keep components as subdirs: conjurer_librarian, conjurer_musician,
  spotify_dl, yt_dlp, fonts, utils, docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 23:50:55 +02:00
gitea c4fa88e8ee Legacy remove 2025-10-29 13:54:58 +01:00
gitea a8e32d6b05 test 2024-11-06 20:42:27 +01:00
gitea 02cd2ba5df fx 2024-11-06 20:28:33 +01:00
gitea 46de87e874 fix 2024-11-06 20:25:06 +01:00
gitea 7197309e1d Test 2024-11-06 20:19:52 +01:00
gitea b82dfe38da Test 2024-11-06 19:41:57 +01:00
gitea 4448b079e6 tst 2024-11-06 19:39:40 +01:00
gitea f45dc906ac test mic 2024-11-06 19:35:52 +01:00
gitea c33cfc27ba New stuff incoming 2024-10-03 13:47:02 +02:00
gitea dd0b679bfc fx 2024-09-19 16:55:09 +02:00
gitea 89f4b0e51e tst 2024-09-17 21:48:30 +02:00
gitea e6648926a6 Fx 2024-08-15 17:12:01 +02:00
gitea ce9beb5ecf fx 2024-08-15 16:54:08 +02:00
gitea 63fb7a3b8a fx 2024-08-15 16:51:24 +02:00
gitea fd23518f7a Tst 2024-08-15 16:46:42 +02:00
gitea 5ea792bcd2 Transcribe cog 2024-08-15 14:11:17 +02:00
gitea c31390859f Metadata 2024-08-09 16:43:52 +02:00
gitea 314a77c1f9 Fx 2024-08-05 11:24:13 +02:00
gitea 4842ccdc22 Music trackin upgrade 2024-08-03 15:34:56 +02:00
gitea 803ad01a83 bgfx 2024-08-02 12:12:12 +02:00
gitea 81d5fcfb8f Now playing 2024-08-02 12:10:55 +02:00
gitea 4d79c51e20 Fx frmt 2024-08-02 12:07:17 +02:00
gitea f320b20850 fx frmt 2024-08-02 12:02:21 +02:00
gitea c39900e15f Fx frmt 2024-08-02 12:01:05 +02:00
gitea 0f7e5d4a25 Fix 2024-08-02 11:53:03 +02:00
gitea ce91aec6ff Fix 2024-08-02 11:50:54 +02:00
gitea 759c06aa50 Buffx 2024-08-02 11:47:13 +02:00
gitea 1ebd58b8ac UPdate for music tracker 2024-08-02 11:43:38 +02:00
gitea b87cf7f4d8 Changes for too long messages handling 2024-07-22 16:20:06 +02:00
gitea a68745713b Change in weights 2024-07-20 13:27:40 +02:00
gitea bbe1b89007 Fix logging0 2024-07-20 11:13:36 +02:00
gitea 27fefdccb5 Added more logging 2024-07-20 09:55:32 +02:00
gitea f2fb2232ee Audition 2024-07-20 09:09:14 +02:00
gitea 408f6e5c4e fix 2024-06-19 19:47:15 +02:00
gitea a266e5d240 Skip track 2024-06-19 19:45:36 +02:00
gitea 917b57c586 Fix 2024-06-19 18:55:48 +02:00
gitea 0e15218abd Test 2024-06-19 18:53:37 +02:00
gitea f56773b2e1 test2 2024-06-19 18:46:40 +02:00