develop #115

Merged
adrian.tkacz merged 4 commits from develop into main 2026-09-24 13:33:16 +00:00
Owner
No description provided.
feat(worker): compact first-run setup for the packaged Windows executable (issue #113)
All checks were successful
ci / check (pull_request) Successful in 6m51s
91982aecdb
The v0.1.0 Windows ZIP dead-ended: scarcity-worker.exe told users to run
the Python console script the package does not contain. This makes the
documented first-run dialog claim (docs/m10-acceptance.md) true:

- worker_setup (library, GUI-independent): typed versioned non-secret
  local settings over the existing WorkerLocalStore (strict parse,
  fail closed, atomic); pairing orchestration over the EXISTING
  WorkerRuntime.pair shared by dialog and CLI; deterministic error
  mapping; loopback-only Ollama settings; documented CLI-vs-settings
  precedence; first-run/tray launch routing.
- windows_tray: entry router (pair / run / no-arg first-run), tray
  runtime factory rebuilt from stored settings on every reconnect,
  Worker settings action wired to the same dialog, best-effort parent
  console attach for packaged CLI output (CreateFileW handle, never a
  filesystem write), exe-named remediation text.
- packaging: tkinter setup dialog adapter beside the pystray tray view
  (lazy imports; first-run + settings modes in one compact dialog);
  spec pins the tkinter hidden import; launcher wires both seams.
- tests: discriminating first-run suite (entry routing, cancel and
  partial-failure semantics, real pairing path, code non-persistence,
  safe errors, settings strictness/precedence/restart, pair-only
  validity, loopback-only Ollama, packaged pair command, no Windows
  Codex advertising) + packaging spec/adapter shape updates.
- docs: README Windows package instructions, M10 acceptance record,
  D-051 decision record.
fix(worker): close the worker store exactly once on every exit path (issue #113)
All checks were successful
ci / check (pull_request) Successful in 6m56s
3aea980efb
The packaged Windows pairing path returned from typed pairing failures
without closing the WorkerLocalStore it opened - invalid/expired/used
codes, TLS and transport failures, origin validation all leaked. The
same inspection proved the identical defect in the analogous paths:
tray_main's tray-runtime store (leaked on config errors, never closed
on the success path) and worker_client.main (pair failures, run
configuration errors and raising run() leaked past the outer except).

All three sites now own their store explicitly: open -> try -> finally
close, exactly once on success, normal failure and exceptional failure,
never via GC or process exit, and never before the runtime that uses
the store has finished. Pairing semantics, messages, exit codes,
redaction and TLS verification are unchanged.

Tests: CloseCountingStore records close() calls; 21 new ownership
tests across the packaged pair command, the tray body and
worker_client.main assert close_count == 1 per path (16 of them
verified to fail against the pre-remediation code), and assert the
store is still open while the runtime runs. Focused suites emit zero
ResourceWarnings with PYTHONWARNINGS=error::ResourceWarning.
fix(worker): one deliberate owner per Windows GUI event loop (D-052, issue #113)
All checks were successful
ci / check (pull_request) Successful in 6m55s
bc2cb15639
Live Windows retest: the settings dialog froze, lost focus transitions
and occupied the tray callback. Root cause: the dialog's tkinter
mainloop ran synchronously inside the pystray menu callback - one
toolkit's event loop nested inside another toolkit's message loop - so
the tray's Win32 loop could not service any action while the dialog
was open and Quit could not tear the process down deterministically.

New ownership model (D-052, docs/decisions.md): every long-lived event
loop has exactly one owner. The tray thread owns the Win32 message
loop and its menu callbacks only marshal or signal; ONE UI-owner
thread (windows_tray.UiDispatcher) owns every tkinter root, created,
mainlooped and destroyed strictly serially; the runtime keeps its
session thread; the process main thread coordinates restart/quit.
Crossings are asynchronous: dispatcher posts, threading events, the
store's own lock. A non-blocking settings slot taken at post time
makes repeat/rapid requests unable to open a second window; a close
signal plus the view's arm_close_request capability lets Quit unwind
an open dialog and join the UI thread with a bound before tray_main
returns - no orphan GUI threads, no daemon-kill reliance.

Shell/browser/folder launches and status notifications never run on
the tray loop. Onboarding semantics, pairing, messages, exit codes,
redaction and TLS verification unchanged; no new framework, process
or IPC layer; the library stays GUI-toolkit-free.

Tests: TrayUiOwnershipTests pins the model - the settings action
returns immediately, dialogs run on the UI-owner thread, five rapid
requests open exactly one window, Quit/Reconnect with the dialog open
are deterministic, Settings->Cancel->Settings->Save->restart and
Settings open->Quit sequences hold, and no UI/session thread survives
tray_main. Reviewer: independent GLM-5.3-Flash (thinking MAX) APPROVE
with hardening defers (slot/release order, enqueue-after-poison race)
applied in this commit.
Reviewed-on: BioMedical-IT/scarcity-router#114
Reviewed-by: Adrian Tkacz <adrian.tkacz@creatidy.com>
Sign in to join this conversation.
No description provided.