Commit Graph

19 Commits

Author SHA1 Message Date
gitea 9f22dbf94b Librarian: 'still searching' heartbeat every 20 min
CI / compile (pull_request) Successful in 18s
CI / unit (pull_request) Successful in 39s
CI / integration (pull_request) Failing after 1m2s
CI / compile (push) Successful in 14s
CI / unit (push) Successful in 34s
CI / integration (push) Successful in 37s
build / build (push) Successful in 33s
A deep scan runs for hours with nothing in the log between start and
finish, so it's impossible to tell a working search from a wedged one.
Every CONJURER_LIBRARIAN_HEARTBEAT_SECONDS (default 1200 = 20 min) a
running search now logs that it is still going, with its uuid, the search
phrase, hits so far, elapsed minutes, and a rough how-far-along.

The estimate is deliberately cheap: the producers ALREADY record a byte
offset per chunk file (the resume watermarks), and the total size is
stat()'d once per search when the chunk list is discovered. A reading is
then just a sum over ~40 ints - nothing extra happens per line, and no
cycles are spent estimating how many cycles are left.

search_for_doi takes an optional progress dict it fills with the live
positions dict + total_bytes; the librarian publishes the running search
(uuid/query/progress/live hits) while the scan runs and clears it in
finally. Nothing running => the heartbeat stays quiet.

Tests: percentage maths incl. unknown-total and >100% clamping, the
register/clear round-trip, and an end-to-end check that a real scan fills
progress so the offsets cover the chunk files on disk. Suite: 58 unit +
65 integration green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 12:09:46 +02:00
gitea ac16b77f56 Librarian: graceful shutdown with resumable search state
CI / compile (pull_request) Successful in 10s
CI / unit (pull_request) Successful in 28s
CI / integration (pull_request) Successful in 27s
build / build (push) Successful in 42s
CI / compile (push) Successful in 10s
CI / unit (push) Successful in 26s
CI / integration (push) Successful in 25s
A restart of the librarian used to throw away an in-flight search (and any
searches still queued). Now search state survives a restart:

* Resumable DB scan (search_bot): each producer records a tell()-cookie
  watermark per chunk file as it goes (safe because search_for_doi drains
  the work queue before returning), and can seek back to it. search_for_doi
  now takes stop_event + resume and returns (result_list, positions,
  interrupted).

* Persisted requests: /query writes the accepted request to a disk queue
  before enqueuing; replay_requests re-enqueues unfinished ones on startup.
  So even a search still waiting in the queue survives a restart.

* Checkpoints: when a graceful shutdown interrupts a scan, the librarian
  writes {dois, found-so-far, per-file offsets}. On restart answer_query
  loads it, skips the (already done) Crossref+refine, and continues the
  scan from the saved offsets with the found DOIs pre-marked - no line is
  read twice and none is missed. A finished or crashed search forgets its
  request+checkpoint (no poison-pill replay).

* Graceful shutdown: SIGTERM/SIGINT set a shutdown event; the running scan
  checkpoints and the worker stops. The main thread then exits within a
  BOUNDED window (CONJURER_LIBRARIAN_GRACEFUL_TIMEOUT, default 45s) so the
  pod can never become an un-killable zombie. Needs terminationGracePeriod
  >= that in the deploy (separate PR).

Tests: search_bot resume correctness (seek past scanned, don't miss/re-scan;
stop_event -> interrupted) and librarian state mechanics (request replay,
forget, checkpoint round-trip, poison-pill drop). Suite: 58 unit + 49
integration green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 22:09:49 +02:00
gitea c5643aa28f Librarian: drop write-only result dumps + tame search logging
CI / compile (pull_request) Successful in 9s
CI / unit (pull_request) Successful in 23s
CI / integration (pull_request) Successful in 27s
build / build (push) Successful in 33s
CI / compile (push) Successful in 13s
CI / unit (push) Successful in 33s
CI / integration (push) Successful in 27s
Two hygiene fixes on top of the work-queue OOM bound:

Result dumps: cr_results / rr_results / s_results.json were write-only
(nothing reads them) yet accumulated EVERY search forever and json.load'd
the whole growing file on each write - unbounded RAM and PVC growth, and
for a deep search the raw cr_results dump is hundreds of MB. They are now
off by default (CONJURER_LIBRARIAN_DEBUG_DUMPS) and, when enabled, are
overwritten with just the latest search - never loaded or accumulated.
not_in_db.json is untouched: it's a real queue the scraper drains.

Search logging: search_bot logged via print(), including a per-line
carriage-return progress line that flooded stdout / the log file with
millions of entries - fine for a desktop app, unreadable and bloating in
a container. All of it is now proper logging at DEBUG (with coarse
per-500k-line progress), so a normal run is quiet. The librarian log
level is configurable (CONJURER_LIBRARIAN_LOG_LEVEL, default INFO) and a
stdout handler is added so  stays useful now that the
search no longer prints straight to stdout. Set DEBUG for full verbosity.

Also: make test_result_delivery_contract hermetic (point the durable
spool at a temp dir so it can't pollute or be poisoned by the real
result_inbox/ between runs) and gitignore the runtime spool dirs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 20:19:49 +02:00
gitea 40605b959f Librarian: bound the DOI search work queue to stop OOM-killing the pod
CI / compile (pull_request) Successful in 9s
CI / unit (pull_request) Successful in 17s
CI / integration (pull_request) Successful in 26s
build / build (push) Successful in 23s
CI / compile (push) Successful in 7s
CI / unit (push) Successful in 22s
CI / integration (push) Successful in 26s
The pod restarted spontaneously mid-search (no liveness probe is set, so
it was the kernel OOM-killer against the 1Gi limit). Cause: search_bot
built its work queue with maxsize 35_500_000. The producers stream the
WHOLE DOI database (tens of millions of lines across chunks) into it while
a few consumers drain, so the queue could buffer gigabytes of lines -
blowing the 1Gi container and taking the whole in-flight search with it.

Bound the queue (default 100k lines, env CONJURER_LIBRARIAN_WORKQ_SIZE),
so producers backpressure to consumers and RAM stays in the low MB.
Because a bounded queue means a producer can now block on a FULL queue,
make the producer's put timeout-poll the TERM sentinel, so a full queue
whose consumers have already finished (all DOIs found) can never deadlock
it. New test pins that: tiny queue + target on line 1 + thousands of
trailing decoys still terminates and finds the target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 19:34:56 +02:00
gitea defc482a22 fix: batch A - crash bugs, a leak, and startup/edge fragility
CI / compile (pull_request) Successful in 10s
CI / unit (pull_request) Successful in 19s
CI / integration (pull_request) Successful in 10s
build / build (push) Failing after 46m40s
CI / compile (push) Successful in 20s
CI / unit (push) Successful in 19s
CI / integration (push) Successful in 16s
Seven confirmed defects from the code audit, each small and low-risk.

* ai_functions.get_random_cyclic_message: random.randint(0, len(CYCLIC_WORDS))
  is inclusive -> could return len -> IndexError. Now randrange(len) + guard on
  an empty CYCLIC_WORDS.
* librarian_commands.get_image_sadox: random.randrange(0, len(res)-1) never
  picked the last comic and raised ValueError('empty range') on a single file.
  Now randrange(len) + an empty-dir guard.
* ai_commands image generation: every DALL-E error branch replied but did not
  return, so control fell through to `if response:` with response unbound ->
  UnboundLocalError right after the friendly message. Each branch now returns;
  response is pre-initialised; and PermissionDeniedError no longer passes a
  (message, text) tuple as a single arg.
* search_bot DOI match: `item["DOI"] in data` was a substring test, so a DOI
  that is a prefix of a longer one (10.1/1 vs 10.1/12) produced a false 'exists'
  hit. Now matches the line's first whitespace token exactly, via an O(1) dict
  index built once per consumer (also removes the O(queried-DOIs) per-line scan
  - a real win for large databases).
* communication_subroutine.scan_incoming: matched records were never removed
  from awaiting_q, so it grew unbounded over uptime and a reused UUID could
  re-match a stale record. Matched records are now dropped after dispatch.
* communication_subroutine.id3: (resp.headers.get("icy-name") or "").title()
  guards against a stream that omits headers (was AttributeError on None,
  500-ing the /prepped_tracks "next" handler).
* betoniarka.scan_tracks: waits for the radio logs to exist instead of dying
  with FileNotFoundError on a fresh deploy (which silently killed the
  now-playing forwarder until a restart).

Verified: tests/unit/test_search_bot.py gains exact-match and trailing-metadata
cases; full unit job 43 passed. Remaining observations (image-gen stale
/home/pi fallback paths + dead FileNotFoundError-after-OSError branch; tailer
still vulnerable to mid-run log rotation; DOI-first-token assumption) noted for
follow-up - none are crashes on the normal path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 23:01:27 +02:00
gitea 031c1f8aea librarian: tolerate bad bytes in chunks; log what the result-send does
CI / compile (pull_request) Successful in 10s
CI / unit (pull_request) Successful in 20s
CI / integration (pull_request) Successful in 15s
CI / compile (push) Successful in 29s
CI / unit (push) Successful in 30s
CI / integration (push) Successful in 21s
build / build (push) Failing after 7s
Two field-reported robustness gaps on top of the hang fix.

1) A stray non-UTF-8 byte in a chunk (0x96 in the report) raised
UnicodeDecodeError from readline() - which is a ValueError, so the earlier
`except OSError` did NOT catch it. The finally-sentinel meant no hang, but the
producer died mid-file with a loud traceback and every DOI after the bad byte
went unsearched. Now chunks are opened with errors="replace" (bad bytes become
U+FFFD; DOIs are ASCII so a match is never affected) so the read runs to EOF,
and the producer's except is broadened from OSError to Exception so no per-file
error can ever crash the thread - it's logged and the sentinel still fires.

2) The result-send back to the bot (BackgroundTaskSearch._run) now logs exactly
what goes out - target URL, uuid, DOI count and the DOI list - so the librarian
log plainly shows a result was sent and what was in it. And a failed POST is no
longer fatal: a RequestException used to propagate out of the worker loop and
kill the thread, stalling every future query until restart; it's now caught and
logged, and a non-200 from the bot is logged as a warning.

Verified: tests/unit/test_search_bot.py gains a case writing a chunk with a 0x96
byte before a valid DOI and asserting that DOI is still found (file read to
completion, not aborted). All 5 search_bot unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 15:59:07 +02:00
gitea 1a59c9f6c5 librarian: stop the DOI search from hanging on a chunk-count mismatch
CI / compile (pull_request) Successful in 1m26s
CI / unit (pull_request) Successful in 1m8s
CI / integration (pull_request) Failing after 10h21m8s
CI / compile (push) Successful in 12m43s
build / build (push) Failing after 13m30s
CI / unit (push) Successful in 2m32s
CI / integration (push) Failing after 1h54m10s
search_bot conflated MAXTHREADS into two jobs at once - how many chunk files to
read (files 0..MAXTHREADS-1) AND how many producer sentinels to wait for - so
the two had to match exactly. Set too low it silently skipped trailing chunks;
set too high (or with any chunk missing/unreadable) a producer crashed before
emitting its sentinel, the consumers' count never reached the threshold, and
search_for_doi hung on join() forever. The idle-timeout failsafe that was meant
to break a starved consumer was dead code: `if empty_counter > 5: ... elif
empty_counter > 10: break` - >10 implies >5, so the elif never ran.

Fix, three layers:
* auto-discover the chunk files present (discover_chunk_files: <n>_chunk.txt in
  numeric order) instead of range(0, MAXTHREADS). All files are read regardless
  of count, and no producer is ever pointed at a missing file;
* the sentinel threshold is now the number of producers actually started, so it
  can't drift from what's emitted;
* producers emit their sentinel in a finally, so even a crash (missing/unreadable
  chunk) can't starve the count; and the idle backstop is reordered so it can
  actually fire (>EMPTY_LIMIT seconds) as a last resort.

MAXTHREADS is deprecated and unused (kept only so old env files don't break);
docs/env updated to say chunk files are auto-discovered.

For the reported case (MAXTHREADS=40, files 0..43): before, files 40-43 were
silently never searched, and any run that referenced a missing chunk hung
forever. After, all 44 are searched and it always terminates.

Verified in a pytest-only venv (tests/unit/test_search_bot.py): DOI in a
trailing chunk is found; an unreadable chunk still terminates; empty dir returns
at once; discovery is numeric-sorted. Full unit job 27 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 00:15:03 +02:00
Michal Tuszowski fd6427a282 librarian: mount + seed runtime JSON state under /lib_temp_files
(Re-applied cleanly on top of main: PR #18 was rebase-merged, so the branch
that became #19 conflicted on the already-landed commits. This carries ONLY
the librarian state fix - the sole content difference between that branch
and main - so nothing else is touched or lost.)

The worker threads open cr_results/rr_results/not_in_db/s_results.json in
place ('r+'), crashing with FileNotFoundError in a fresh container. New
conjurer_librarian/lib_paths.py resolves all four under a persistent dir
(CONJURER_LIBRARIAN_STATE_DIR, default /lib_temp_files) and seeds missing
ones with '{}' on import; shared by conjurer_librarian.py and scrape_bot.py
(no circular import). ndb_database/database initialised to {} before load so
a corrupt persisted file degrades to empty instead of NameError. search_bot
/search_bot2 open the DOI DB 'r' not 'r+' so /doi can stay read-only. Docker:
STATE_DIR env + VOLUME, compose mounts /srv/librarian/state:/lib_temp_files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 13:40:13 +02:00
Michal Tuszowski 597bc004fc docker: Proxmox deployment for bot/librarian/musician + code fixes
Containerises the three services (each intended for its own Proxmox VM)
and adds the code changes needed to run cleanly on Linux/Docker.

Code fixes:
- constants.py: CONJURER_DATA_DIR roots all writable bot state under one
  mounted volume (per-variable overrides still win; native Pi unaffected)
- conjurer_librarian/search_bot.py + scrape_bot.py: the hardcoded Windows
  DOI database path (C:\Database\chunks\) is now CONJURER_LIBRARIAN_DB_PATH,
  with CONJURER_LIBRARIAN_MAXTHREADS / _CHUNK also env-overridable

Docker:
- docker/Dockerfile.{bot,librarian,musician} + compose.{bot,librarian,musician}.yaml
- docker/env/*.env.example (force-added; real *.env stays gitignored)
- docker/entrypoint.bot.sh seeds default JSON state into /data only when
  absent, so preserved history is never overwritten
- .dockerignore
- docs/deployment/DOCKER_PROXMOX.md: step-by-step runbook incl. preserving
  the existing command/conversation history and cross-VM auth

The bot image uses the vendored yt_dlp/spotify_dl forks (they win on
sys.path over the pip packages), dropping the old sed patching.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:16:56 +02:00
Michal Tuszowski a6c20a0054 ci: replace broken default workflows with compile/unit/integration CI
The two scaffold workflows (Python application / Python package) failed on
every PR: they installed deps from a non-existent requirements.txt, ran
flake8/pytest over the vendored yt_dlp fork (new syntax under the 3.8/3.9
matrix), and collected ad-hoc root scripts — notably test_ai.py, which is
an invalid pasted object dump (not Python).

- Remove python-app.yml / python-package.yml and the junk root scripts
  (test.py, test_ai.py, test_time.py)
- Add .github/workflows/ci.yml with three PR-check jobs:
  * compile     — py_compile every first-party .py (no deps)
  * unit        — pytest on pure logic (conanjurer_functions, constants)
  * integration — boot the Flask services and assert the X-Conjurer-Api-Key
                  auth contract (communication_subroutine + conjurer_musician)
- Add tests/ suite, pytest.ini (testpaths=tests) and conftest.py (sys.path)

Fixes surfaced by the compile gate / needed for the integration job:
- conjurer_librarian/search_bot.py + search_bot2.py: f-string reused the
  same quote ({item["exists"]}) -> SyntaxError on Python < 3.12
- conjurer_musician/media_search_functions.py: made import-safe
  (env-overridable paths, lazy DB load / mkdir) so the service can be
  imported and tested off the Pi

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:24:30 +02:00
gitea 7e4dfbf8cd NEw util 2024-07-20 09:00:51 +02:00
gitea 14df489b38 New deasrc 2024-06-13 09:25:36 +02:00
gitea 9d00e40b41 Starting work on librarian headless mode 2024-04-24 12:00:56 +02:00
gitea 5beb1c6fdd Optimization bug fix #1 2024-04-23 21:37:44 +02:00
gitea 082b25c89c Music tunneling (#7)
* New requirement

* SMall fixes for tunnel

* First step towards cogization

* Tmp chng

* Testing of new functions in conjurer_musician.py and radio_conjurer.liq

* Fixes!

* Fix fix

* Mostly working ?

* Requests

* Fixes

* Fix

* Radio requests

* Radio hardkor

* Fix

* Fixes

* Change log to print

* Clearing fetch failed

* Small fix

* sx

* Fix fix fix

* test

* el fixo stupido

* test

* Maybe fix ?

* FIX!

* Fix fix fix

* FIx of volume
2024-04-23 12:59:33 +02:00
gitea c1054241eb Fixing librarian result (#6)
* Formatting and additional logging for bugfix

* Formatting and additional logging for bugfix

* Change of log location

* fixing_librarian_result

* Hopefully final fix that will be needed

* Temporary

* Glebokie gardlo

* Fix in scrape_bot

* Fix that was needed.

* Scraper settings

* Small fix in scraping

* FIxing bad  naming convention

* Log message fix

* Test of deep search

* Fix in deep search

* Refactoring

* WHat and idiot coded that....
Ooops. That was me. A month ago.

* Refactoring continued

* Bugfix

* FIx

* Not needed after fix
2024-04-21 15:54:34 +02:00
gitea 0ac9006665 Search webservice (#2)
* Deployable script for windows file service

* Deploy!

* Server side work
2024-04-12 23:21:48 +02:00
gitea b08642dd88 Preparation for deployment 2024-04-10 19:12:44 +02:00
gitea 89003c32c9 Serverside work of search 2024-04-10 15:26:40 +02:00