493 Commits

Author SHA1 Message Date
Patrick Buckley 2ab60853f5 chore: bump version to 1.2.2 v1.2.2 2026-04-12 20:41:42 -07:00
Patrick Buckley fed5b96a6f fix: universal tool_call/tool_result orphan detection for OpenAI-comp… (#346)
* fix: universal tool_call/tool_result orphan detection for OpenAI-compat providers

The Anthropic provider had orphan detection for mismatched tool_call ↔
tool_result pairs, but OpenAI-compatible providers (Chat Completions,
Google, Responses API) had none. When an Anthropic model runs behind
an OpenAI-compat API (e.g. Azure) or cancellation creates orphans,
the API rejects the malformed request.

- Rewrite sanitize_messages() with orphan detection: synthesize error
  tool results for unmatched tool_calls, drop tool results with no
  matching tool_call, fill empty tool_call IDs with positional remap
- Call sanitize_messages() from Responses API _convert_messages()

* fix: address review feedback on orphan detection

- Track answered IDs per-turn (local_answered) instead of scanning
  all of out, preventing false matches from reused IDs across turns
- Drop empty-ID tool results that have no remap entry instead of
  passing them through with invalid empty tool_call_id
- Increment empty_result_idx for every empty result, not just remapped
- Remove dead result_ids peek-ahead code
- Add test for repeated tool_call IDs across turns
2026-04-12 20:41:28 -07:00
Patrick Buckley 4a65535e00 fix: accurate token usage tracking for compaction across all providers (#345)
* fix: accurate token usage tracking for compaction across all providers

Anthropic's input_tokens excluded cached tokens, causing massive
under-reporting (e.g. 327 vs 9000 actual) when prompt caching was
active. This prevented auto-compaction from triggering.

- Normalize Anthropic prompt_tokens to total input (input_tokens +
  cache_creation + cache_read), matching OpenAI semantics
- Reset _last_usage per API call so tool-chain iterations get fresh
  usage instead of max()-merging with stale values
- Add mid-turn compaction check during tool chains to prevent context
  overflow before end-of-turn
- Anchor _remaining_token_budget() on provider-reported prompt_tokens
  with local estimates only for the delta since last API call
- Improve _msg_char_count() to include structural overhead (role,
  tool_call_id, tool call IDs) and handle image tokens in calibration
- Emit status after every API call, not just end of turn

* fix: defensive null coercion and index clamping from review feedback

- Add `or 0` to all getattr calls for input_tokens/output_tokens in
  Anthropic provider (streaming + non-streaming) to handle SDK nulls
- Use getattr for non-streaming input_tokens/output_tokens instead of
  direct attribute access for consistency
- Clamp _calibrated_msg_count with min() in _remaining_token_budget()
  to prevent stale state from over-slicing after compaction
2026-04-12 20:41:28 -07:00
Patrick Buckley 519b86f56e chore: bump version to 1.2.1 v1.2.1 2026-04-08 18:06:12 -07:00
Patrick Buckley 024a2e98d2 fix(ui): remove broken hint animation and restore card toggle
The ws-check-hint animation clobbered the fadein's forwards fill,
making the checkbox invisible for 0.6s on card-body click — appearing
as a deselect-then-reselect. Remove the hint, the unused role=checkbox
on the card, and restore the original symmetric toggle behavior.
2026-04-08 18:05:41 -07:00
Patrick Buckley e9c141aba5 fix(ui): improve delete workstream UX and accessibility (#339)
* fix(ui): improve delete workstream UX and accessibility

Card body click no longer deselects (prevents confusing red border loss);
checkbox pulse hint guides users to deselect affordance. Adds keyboard
navigation, aria-labels, hover feedback, animations, and neutral Close
button styling after deletion.

* fix(ui): remove duplicate a11y checkbox from delete-mode cards

Hide the visual checkbox from the a11y tree and tab order so the card
(role=checkbox) is the sole keyboard/screen-reader target. Addresses
Copilot review feedback about nested interactive elements.
2026-04-08 17:20:45 -07:00
Patrick Buckley b038dbdd5b chore(deps): bump lacme to >=1.0.5 (cryptography security update) 2026-04-08 16:48:35 -07:00
Patrick Buckley 98d3289852 chore: bump version to 1.2.0 v1.2.0 2026-04-07 00:38:05 -07:00
Patrick Buckley 2025bf8a6f perf: reduce initial rebalance from ~1.5s to ~50ms on PostgreSQL (#334)
* perf: reduce initial rebalance from ~1.5s to ~50ms on PostgreSQL

Increase seed_ring_buckets chunk sizes (PG 500→16k, SQLite 500→8k) to
cut network round-trips from 131 to 5. Add ConsoleRouter.populate_from_assignments()
to build the routing cache directly from computed assignments, eliminating the
65 536-row DB read-back. Router becomes ready in <1ms; DB persistence follows.

* fix: address review — populate after seed write, sync router version

Move router cache population after seed_ring_buckets() so the router
is never "ready" with an unpersisted ring. Pass the new rebalancer
version to populate_from_assignments() so check_version() on the
collector thread does not trigger a redundant 65 536-row refresh.
2026-04-07 00:35:55 -07:00
Patrick Buckley 100bb02e3b fix: stale ARIA attrs after promote, deferred DELETE on pre-ID dismiss
- Remove role="status" and aria-label during _promoteQueuedMessages
  so screen readers don't announce stale "queued" context
- Mark element with pendingDismiss when user dismisses before msg_id
  arrives; send deferred DELETE when the send response provides the ID
2026-04-06 22:35:23 -07:00
Patrick Buckley 2b3b229da6 fix: flush queued messages on normal completion (no tool calls)
If the model responds without tool calls, the main loop exits
immediately — no tool-result seam exists for advisory injection.
Queued messages were silently orphaned in the OrderedDict. Now
flushed as regular user messages before emitting idle state.
2026-04-06 22:34:00 -07:00
Patrick Buckley 76ecb99374 fix: queued message promote loop and dismiss behavior
Bug 1: Extract _promoteQueuedMessages() — removes badge, dismiss
button, queued classes, and data-msgId. Called from setBusy(false)
on state_change: idle.

Bug 2: _dequeueMessage no longer removes the DOM element when server
returns not_found (message already injected). Only removes on
"removed" (actually dequeued). Network errors also preserve the
element. The promote loop handles cleanup on idle instead.
2026-04-06 22:28:21 -07:00
Patrick Buckley c578051cb8 feat: tool result advisory system with user message queuing (#333)
* feat: tool result advisory system with user message queuing

General-purpose advisory injection for tool results — when advisories
are present, tool output is wrapped in <tool_output> tags with
<system-reminder> blocks appended. Two initial producers:

- Output guard advisories: model sees why content was flagged/redacted
- User message interjections: users can queue messages mid-execution
  via the web UI, injected at the next tool-call seam

Queued messages use !!! prefix for important priority. Advisory
injection is gated by ModelCapabilities.supports_tool_advisories
(default true for commercial models, false for local/vLLM).

On cancel/error, queued messages are flushed as regular user messages
so nothing is silently lost. Raw tool output (pre-wrap) is persisted
to the DB to keep history clean of ephemeral advisory XML.

* fix: frontend UX for queued messages — rollback, discoverability, a11y

- Send button changes to "Queue" (outline style) during busy state,
  visually distinct from filled red Stop button
- Placeholder updates to hint at !!! priority convention
- addQueuedMessage returns element ref for optimistic UI rollback
- Remove queued element on queue_full, busy, or connection error
- Add role="status" and aria-label to queued message elements
- Promote queued messages to normal appearance when generation ends

* feat: queued message removal via dismiss button

Switch backing store from queue.Queue to OrderedDict + Lock for O(1)
removal by ID. Each queued message gets a UUID, returned to the
frontend and stored as data-msg-id on the DOM element.

Dismiss button (x) on queued messages calls DELETE /v1/api/send with
the msg_id. If the message was already injected (race), server returns
not_found and the UI removes the element anyway.

No new endpoint — DELETE method added to the existing /v1/api/send
route. dequeue_message() on ChatSession is O(1) under the lock.

* fix: address PR review — escaping, types, list output, message cap

- Escape </tool_output> and <system-reminder> in tool output to prevent
  wrapper tag injection from untrusted tool results
- Change _collect_advisories return type from list[Any] to list[ToolAdvisory]
- Drain queued messages on list/structured output (append as text part)
  so they aren't silently stuck until a str result appears
- Cap queued message length at 2000 chars to prevent context bloat
- Remove unused var in _dequeueMessage
2026-04-06 21:51:47 -07:00
Patrick Buckley 701c3fc717 chore: bump version to 1.2.0a5 v1.2.0a5 2026-04-06 15:52:05 -07:00
Patrick Buckley 92ad5bd439 Feat/tab action dropdown (#332)
* feat: replace workstream action buttons with per-tab dropdown menu

Move refresh-title, edit-title, fork, close, and delete actions from
the header toolbar into a dropdown menu on each workstream tab,
triggered by a ▾ chevron that replaces the × close button.

Dropdown follows the existing pane context menu pattern: keyboard
navigation, mutual exclusion, click-outside/Escape dismiss, toggle
on re-click, aria-expanded + aria-haspopup, and focus restoration.

Delete is visually distinct (red text + wash + red focus ring, 6px
separator). Mobile hides "Refresh title" and sizes the chevron to
36px touch targets.

Removes updateWsActionButtons(), _applyTitleButtonState(), and
_wsTitleState tracking (dead code after button removal).

* fix: remove Ctrl+Shift+R shortcut that overrides browser hard refresh

Refresh title is a low-frequency action accessible from the tab
dropdown; no replacement keybind needed.

* fix: address tab dropdown review findings

- Pass wsId through dropdown actions so they target the correct
  workstream even when opened on a non-active tab
- Fix setTimeout race where closeTabDropdown before timeout fires
  could leave stale listeners
- Guard Close and Delete on last workstream (dropdown, keyboard
  shortcuts, and defense-in-depth in confirmDeleteWorkstream)
- Use aria-disabled instead of disabled so screen reader users can
  discover unavailable items via arrow keys
- Enlarge chevron hit target, add hover affordance with subtle
  background highlight
- Add 0.1s dropdown open animation (respects prefers-reduced-motion)
2026-04-06 15:48:13 -07:00
Patrick Buckley 58c81b2b46 fix: resolve CodeQL double-import findings in test files (#331) 2026-04-06 14:18:02 -07:00
Patrick Buckley a2d4598012 fix: address CodeQL findings — BaseException and empty except (#330)
- server.py: catch (Exception, GenerationCancelled) instead of
  BaseException so KeyboardInterrupt/SystemExit propagate normally
- judge.py: log client close failures instead of bare pass
2026-04-06 13:53:26 -07:00
renovate[bot] 4f83dba1b9 chore(deps): lock file maintenance (#326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 13:37:33 -07:00
dependabot[bot] 2629f217d2 chore(deps-dev): bump vite from 8.0.4 to 8.0.5 in /sdk/typescript (#329)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.4 to 8.0.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-06 13:14:32 -07:00
Patrick Buckley d1162b2eb9 fix: preserve Gemini thought_signature via provider_blocks fidelity lane (#328)
Gemini's OpenAI-compat endpoint requires thought_signature to survive
the tool-call round-trip. Previously dropped because the Chat Completions
provider cherry-picks only standard fields (id, type, function).

Fix: GoogleProvider now captures raw tool-call dicts (including
thought_signature) via provider_blocks — the same fidelity lane the
Anthropic provider uses for signature round-tripping. On the next turn,
_prepare_messages reconstructs tool_calls from the stored raw data and
strips _provider_content so it never reaches the wire.

Changes:
- _openai_chat.py: add _prepare_messages and _extract_tool_calls hooks
- _google.py: override hooks + tap-pattern _iter_stream for streaming
- model_registry.py: auto-detect .googleapis.com → google provider
- session.py: read cancel_on_approval from ConfigStore
- console/server.py: add PUT/DELETE to proxy route methods
- server.py: fix fork naming (don't inherit source display name)
2026-04-06 13:13:38 -07:00
Patrick Buckley 217688547e fix: expose channel gateway port for bare-metal deploys
The channel gateway registers with its Docker-internal hostname
(e.g. http://channel:8091) which is unreachable from a host-side
server. Publish port 8091 and set TURNSTONE_CHANNEL_ADVERTISE_URL
to localhost so the server can reach it for schedule notifications.
2026-04-06 10:47:50 -07:00
Patrick Buckley 5dc98f75fb fix: scheduled task notifications not delivered on cancellation
GenerationCancelled extends BaseException, not Exception, so it bypassed
the except handler in _run_initial. The finally block ran but
_extract_last_assistant_content returned "" (response never appended to
messages), and _fire_notify_targets bailed on the empty content guard.

Fixes:
- Catch BaseException (not just Exception) in _run_initial so
  GenerationCancelled is handled and the UI state is cleaned up
- Remove the empty-content suppression in _fire_notify_targets —
  scheduled tasks should always deliver, even with a fallback message
  when no output was captured
2026-04-06 09:54:03 -07:00
renovate[bot] 6980ba5aae chore(deps): lock file maintenance (#325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 04:39:13 -07:00
Patrick Buckley 57912faa52 chore: bump version to 1.2.0a4 v1.2.0a4 2026-04-06 03:58:42 -07:00
Patrick Buckley 0625fac87b fix: shorten judge model dropdown default label 2026-04-06 03:57:21 -07:00
Patrick Buckley dc3a1b7a64 fix: workstream toolbar UX — relocate to tab bar, fix visibility and theme sync
- Move action buttons (refresh/edit/fork/delete) from header to tab bar,
  grouped in #ws-action-group with separators. Contextually adjacent to
  the workstream tabs they operate on.
- Toggle group visibility via CSS class (.hidden) instead of per-button
  inline style.display — makes media query overrides reliable.
- Call updateWsActionButtons() from renderTabBar() so buttons appear on
  initial load and ws_created, not just on tab switch.
- Fix theme loss between nodes: loadInterfaceSettings no longer overwrites
  localStorage with server defaults — preserves user's theme choice when
  switching nodes via console proxy.
- Add flex-shrink:0 on +/split buttons to prevent squeeze with many tabs.
2026-04-06 03:54:03 -07:00
Patrick Buckley a3140da3a5 docs: update documentation for PRs #312-#316 (#324)
- README: add Google Gemini to multi-provider feature list and requirements
- architecture.md: add GoogleProvider, update supported provider values,
  file listing, config example
- judge.md: document cancel_on_approval, fresh-client lifecycle, fallback
  delivery, Google compatibility
- settings.md: add judge.cancel_on_approval, new interface.* section
  (close_tab_action, theme), update total count
- api-reference.md: document 6 new workstream/settings endpoints,
  add judge_model to workstreams/new
- console.md: add judge model to modal fields, add keyboard shortcuts
- console_schemas.py: add judge_model field to ConsoleCreateWsRequest
- server_spec.py: add 6 new EndpointSpec entries
- diagrams: add GoogleProvider to package structure and class diagram
2026-04-06 03:43:12 -07:00
Patrick Buckley 8838bd0f8d fix: apply model.default_alias on model-reload by refreshing ConfigStore
The model-reload handler read model.default_alias from ConfigStore's
in-memory cache, which could be stale if the earlier best-effort
config-reload notification failed or hadn't arrived yet. Force a
cs.reload() from DB before reading the alias. Also publish config
changes from the console before dispatching model-reload, and
downgrade the misleading "No 'default' model alias" log to debug.
2026-04-06 03:37:35 -07:00
Patrick Buckley 7f63cd2d33 feat: add keyboard shortcuts for workstream actions (#323)
Ctrl+Shift+R  Refresh title (regenerate via LLM)
Ctrl+Shift+E  Edit title
Ctrl+Shift+F  Fork workstream
Ctrl+Shift+X  Delete workstream (X not D — avoids Chrome DevTools conflict)

Shortcuts are blocked when any modal is open (edit-title, delete-ws,
batch-delete, new-ws). Help dialog (?) updated with the new bindings.
2026-04-06 03:11:06 -07:00
Patrick Buckley 24f59a6c53 feat: add per-node metadata with auto-collection, admin API, and cons… (#318)
* feat: add per-node metadata with auto-collection, admin API, and console UI

Adds a normalized node_metadata table for structured per-node key/value
metadata with source tracking (auto/user/config).  Auto-populated fields
(hostname, OS, arch, interfaces, cpu_count) are collected at server startup
via stdlib; user-defined fields are managed through the admin API, CLI, or
config.toml [metadata] section.

Storage: migration 035, 7 new protocol methods (get, get_all, set,
set_bulk, delete, delete_by_source, filter), both SQLite and PostgreSQL
backends.  Filtering uses single-query GROUP BY/HAVING for efficiency.

Console API: GET/PUT/DELETE endpoints under /admin/nodes/{node_id}/metadata
with auto-source protection.  cluster_nodes gains meta.* query param
filtering; cluster_node_detail attaches metadata to responses.

Frontend: new Nodes admin tab with collapsible per-node sections, inline
add form, delete with confirmation.  Read-only metadata panel in node
detail drill-down.  Proper design token usage, accessibility (ARIA,
keyboard nav, screen reader labels), and mobile responsiveness.

CLI: turnstone-admin list-node-metadata, set-node-metadata, and
delete-node-metadata subcommands.

64 tests (25 storage, 19 node_info, 20 existing unaffected).

* fix: resolve CI typecheck and test failures

- Fix mypy error: use %-style format string instead of structlog kwargs
  for standard Logger.warning() in console server
- Fix test_get_nodes assertion to include new node_ids=None parameter
- Add debug logging to _collect_interfaces empty except block

* fix: address Copilot review feedback on node metadata

- Clear stale auto/config metadata before upserting on startup
- Wrap metadata filter in try/except with graceful fallback
- Add metadata field to NodeDetailResponse schema
- Use _VALID_NODE_ID regex for consistent node_id validation
- Defensive JSON decode in admin_get_node_metadata
- Switch to read_json_or_400 and require_storage_or_503 helpers
- Add SetNodeMetadataValueRequest for single-key PUT endpoint
- Add bulk GET /admin/node-metadata endpoint (replaces N+1 fetches)
- Update frontend to use single bulk metadata fetch

* feat: add admin.nodes permission scope for node metadata

- Add admin.nodes to builtin-admin role via migration 035
- Switch all node metadata handlers from admin.settings to admin.nodes
- Register admin.nodes in the admin panel permission set
- Node detail metadata panel fetches from cluster endpoint (no admin
  permission needed) instead of admin endpoint

* fix: address second round of Copilot feedback

- Replace inline onclick handlers with data-* attributes and event
  delegation to prevent JS string context XSS
- Move NodeMetadataEntry before NodeDetailResponse and use it as the
  typed metadata field (was list[dict[str, Any]])
- Clean up config metadata on shutdown (was only cleaning auto)
2026-04-06 03:08:19 -07:00
Patrick Buckley 5cbc4bc87c feat: bulk message insert for fork performance + endpoint tests (#322)
Add save_messages_bulk() to StorageBackend protocol and both backends.
Fork path now inserts all messages in a single transaction instead of
N individual save_message() calls — for a 200-message workstream this
goes from 200 connection/insert/commit cycles to 1.

FTS5 indexing is intentionally skipped for bulk fork data (historical
messages indexed on rebuild). Ordering preserved via auto-increment id
with a shared timestamp across all rows in the batch.

Also adds 22 endpoint tests covering the 6 new workstream management
endpoints (delete, open, title, refresh-title, list/update interface
settings) and 4 storage-level tests for the bulk insert path.
2026-04-06 02:54:34 -07:00
renovate[bot] eba2f29cd1 chore(deps): lock file maintenance (#320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 02:40:55 -07:00
Patrick Buckley 66c856eb6e fix: post-merge follow-ups for PRs #312-#316 (#319)
Security:
- Add write scope rules for 4 new workstream POST endpoints
  (delete, open, refresh-title, title) in required_scope() —
  both direct and console-proxied paths

Judge:
- Restore cancel_event check in inner poll loop (was removed)
- Fix fallback delivery off-by-one: items[idx+1:] not items[idx:]
- Skip empty-response retry when finish_reason=="length"
- Reset empty_retries counter after non-empty response
- Document per-turn timeout semantics in JudgeConfig

Google provider:
- Add default base_url for Gemini endpoint in create_client()
- Bump max_output_tokens 8192→65536, set token_param="max_tokens"
- Add api_key detection for googleapis.com in console detect
- Add provider badge CSS (green) and openai-compatible (dim)

Theme:
- Fix POST→PUT for settings persistence (was silently 405-ing)
- Consolidate dual localStorage keys with backwards-compat read
- Lower banner z-index 9999→200, raise login overlay to 10001
- Fix undefined --bg-input, banner contrast for WCAG AA
- Add smooth theme transition with prefers-reduced-motion override
- Console onThemeChange: add title + aria-label updates

Workstream backend:
- Restore close_workstream 400 for last-ws case (was changed to 404)
- Thread-safe _llm_verdicts via _ws_lock on all mutation sites
- Fork: persist tool_calls + provider_data in save_message
- Add get_workstream_metadata to StorageBackend protocol
- Add ChatSession.request_title_refresh() public API
- Use cs.stored_keys() instead of cs._cache
- Redact exception text in delete 500 response
- web_helpers: catch-all logs and returns 500 not 400
- Live-stream ws_created SSE includes title field

Workstream UI:
- Focus traps + Escape on edit-title and delete-ws modals
- Tab close aria-label, mobile breakpoint for action buttons
- Restore name priority (live SSE over stale API)
- Fix double-delete, fork button text, batch delete handler leak
- Optimistic title update, close-last-tab error toast
- ws_id badge show-on-hover, hover states, aria-live, emoji a11y

Console admin:
- Banner aria-labels, judge dropdown wording, detect button class
- New-ws modal Escape handler, provider defaults cross-reference
2026-04-06 02:23:00 -07:00
Patrick Buckley 40a560b39c Merge pull request #316 from sillyWillieBilly/feat/console-enhancements
feat(console): theme-aware banner, judge model support, Google provider in admin
2026-04-06 00:56:39 -07:00
Patrick Buckley bc945852f7 Merge pull request #315 from sillyWillieBilly/feat/ui-enhancements
feat: UI enhancements — workstream management, title editing, fork, delete, theme sync
2026-04-06 00:56:36 -07:00
Patrick Buckley ca70e79d43 Merge pull request #314 from sillyWillieBilly/feat/workstream-management
feat: workstream management — fork, rename, delete, open, interface settings
2026-04-06 00:56:34 -07:00
Patrick Buckley ebcfb56f0e Merge pull request #313 from sillyWillieBilly/feat/judge-improvements
feat: harden judge with fresh-client lifecycle, fallback delivery, and Google compatibility
2026-04-06 00:56:26 -07:00
Patrick Buckley 33d29e3316 Merge pull request #312 from sillyWillieBilly/feat/google-provider
feat: add Google (Gemini) provider adapter
2026-04-06 00:56:08 -07:00
renovate[bot] bfda91cd25 chore(deps): lock file maintenance (#317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 00:19:40 -07:00
William 6fe9f75c3c feat(console): theme-aware banner, judge model support, Google provider in admin
- Replace inline-style console banner with CSS classes + light/dark theme
- Node ID in banner is now a clickable link back to the node UI
- Add judge_model parameter to create_workstream flow
- Add Google to model provider list with default URL
- Provider-specific placeholder hints in model editor
- Detect results populate model name suggestions datalist
- Theme changes in admin settings apply immediately
- Persist theme selection to server via settings API
- Use workstream title field (with name fallback) in collector SSE events
- Add judge model dropdown to new-workstream modal
2026-04-06 08:14:23 +02:00
William c093df274d feat: workstream management — fork, rename, delete, open, interface settings
Add workstream forking (resume with fork=True keeps new ws_id), custom
naming via aliases, title refresh via LLM, and workstream deletion.

New server endpoints: delete, refresh-title, set-title, open-workstream,
list/update interface settings.  Verdict caching with SSE replay on
reconnect, display name fallback (alias→title→name) across all
endpoints, judge_model override per workstream, and settings_changed
broadcast on config reload.

New settings: judge.cancel_on_approval, interface.close_tab_action,
interface.theme.  Storage backends updated with name in
list_workstreams_with_history and new get_workstream_metadata method.
2026-04-06 08:14:18 +02:00
William 49cdb3d0d3 feat: UI enhancements — workstream management, title editing, fork, delete, theme sync
Add workstream action buttons in header (refresh title, edit title, fork,
delete) with supporting modals and keyboard shortcuts.

Workstream tabs: always-visible close button, ws_id badge, configurable
close-tab-action (last_used/nearest/dashboard) via interface settings.

Dashboard: batch delete mode with multi-select, saved workstream cards
with ws_id badge, open endpoint for resuming sessions.

Judge display: late-arriving verdict toast when DOM element is gone,
worst-case verdict glow across all tool calls in approval block.

Theme: server-persisted via admin settings API, real-time sync across
clients via SSE settings_changed events.

New workstream modal: judge model dropdown for per-workstream judge
model selection.
2026-04-06 08:14:14 +02:00
William 04c62f90ff feat: harden judge with fresh-client lifecycle, fallback delivery, and Google compatibility
- Create fresh HTTP client per evaluation run to avoid stale connections
- Store client factory args instead of client instance for on-demand creation
- Add cancel_on_approval config: when True, abort remaining items on user
  approval; when False (default), run all evaluations to completion
- Always deliver LLM verdicts via callback (or fallback when LLM returns None)
- Add _deliver_fallbacks helper for cancelled/incomplete evaluations
- Skip read-only tools for Google provider (requires thought_signature)
- Flatten conversation history to plaintext transcript in _prepare_context
  to avoid multi-turn role sequence errors with strict providers like Google
- Use per-turn timeout instead of shared budget so slow turns don't starve
  later ones
- Add empty-response retry logic (up to 3 retries without consuming turns)
- Enhanced structured logging throughout judge pipeline
- Update tests to match new signatures and behavioral changes
2026-04-06 08:14:09 +02:00
William 1bbaf50214 feat: add Google (Gemini) provider adapter
Add GoogleProvider that extends OpenAIChatCompletionsProvider for
Gemini models via the OpenAI-compatible /v1beta/openai/ endpoint.

- New _google.py with 2M context window defaults and vision support
- Lazy-initialized singleton in create_provider() (thread-safe)
- Route 'google' through OpenAI SDK in create_client()
- Return empty list from list_known_models() (Google models change frequently)
2026-04-06 08:14:05 +02:00
renovate[bot] 38e49b6f9c chore(deps): lock file maintenance (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-05 22:10:44 -07:00
Patrick Buckley 99b0e8db12 chore: bump version to 1.2.0a3 v1.2.0a3 2026-04-05 18:21:22 -07:00
Patrick Buckley d22f5a4baf feat: reconcile judge admin rule UX with edit, disable, and reset act… (#310)
* feat: reconcile judge admin rule UX with edit, disable, and reset actions

Replace the misleading "Customize" button on built-in rules with a
logically consistent 4-state action model: pure built-in (Disable/Edit),
overridden built-in (Disable/Edit/Reset), disabled built-in
(Enable/Edit/Reset), and custom rule (Enable-Disable/Edit/Delete).

Add edit modals for both heuristic rules and output guard patterns,
reusing the existing create modal form structure. Introduce amber
"Reset" button styling to visually distinguish reversible resets from
permanent deletes. Fix source badge redundancy (disabled built-ins now
show grey "built-in" in SOURCE, red "disabled" in STATUS only). Add
aria-labels and role="listitem" for screen reader support.

* fix: preserve built-in pattern_flags and priority on override

Derive pattern_flags from compiled regex for built-in output guard
patterns in the list API so IGNORECASE and other flags survive the
disable/edit/override round-trip. Carry priority through edit modals
via hidden fields so built-in evaluation order is preserved.
2026-04-05 18:19:47 -07:00
renovate[bot] da5eae5352 chore(deps): update dependency katex to v0.16.45 (#309)
* chore(deps): update dependency katex to v0.16.45

* chore: download vendored JS files

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-05 17:57:43 -07:00
Patrick Buckley adb42c66da feat: deliver scheduled workstream results to Discord on completion (#308)
When a scheduled workstream finishes execution, deliver the final
assistant response to configured Discord channels/users via the
existing channel gateway notify infrastructure.

- Add notify_targets column to scheduled_tasks (migration 034)
- Add notify_targets field to Workstream dataclass
- Storage: accept/return/update notify_targets in protocol, SQLite, PostgreSQL
- Server: validate targets, extract last assistant content, deliver via
  gateway with retry, post-completion hook in _run_initial finally block
- Schedule targets override skill notify_on_complete (dedup rule)
- SDK: notify_targets param on async + sync create_workstream
- Console scheduler: pass notify_targets through dispatch
- Console server: schedule CRUD accepts/validates/returns notify_targets
- API schemas: notify_targets on schedule + workstream request/response
- Admin UI: notify textarea in schedule create/edit modals with JSON
  validation, monospace font, aria-describedby hints
- Governance UI: notify_on_complete textarea in skill create/edit with
  client-side JSON validation and field reset on create
- Bounds: max 10 targets, 256 char field limit, gateway response body
  verification matching _exec_notify pattern
- Gateway: 30s asyncio.wait_for timeout on adapter.send to prevent
  hung Discord API calls from blocking the notify endpoint indefinitely
- 39 new tests covering validation, extraction, delivery, dispatch,
  CRUD, and adapter timeout
2026-04-05 17:18:26 -07:00
Patrick Buckley 7968f1b361 feat: auto-invalidate JWT and static assets on version upgrade (#307)
* feat: auto-invalidate JWT and static assets on version upgrade

Add a `ver` claim (major.minor) to user-facing JWTs so tokens from
previous versions are rejected after upgrade, triggering re-login.
Service tokens are excluded for rolling-deployment safety. Tokens
without a `ver` claim (pre-upgrade) are accepted for backward compat.

Inject `?v={__version__}` query strings into static asset URLs at
startup so browsers fetch fresh JS/CSS after any release. Vendored
libraries (KaTeX, Highlight.js, etc.) are skipped since they already
carry version numbers in directory paths. HTML responses now include
`Cache-Control: no-cache` to ensure browsers always revalidate.

Frontend detects upgrade-specific 401s and shows a contextual subtitle
("The server was updated — please sign in again"), then performs a full
page reload after re-auth to load the new versioned assets.

* refactor: address PR review — public API name, single decode, idempotent regex

Rename _version_slot() → jwt_version_slot() to make the cross-module
import explicit rather than relying on a private name.

Move version gating from validate_jwt() into check_request() via a new
AuthResult.token_version field. This eliminates the double JWT decode
that occurred on version-mismatch detection — the token is now decoded
once and the version compared afterward.

Guard version_html() regex against double-apply by excluding URLs that
already contain a query string ([^"?]+ instead of [^"]+).

* feat: structured version_mismatch code, ETag, cross-tab auth sync

Add structured "code": "version_mismatch" field to the 401 response
so the frontend detects upgrade-triggered re-auth without string
matching on the error message.

Add ETag headers to HTML index responses (server, console, and proxied
node UI). Combined with Cache-Control: no-cache, browsers send
conditional GETs and receive 304 between upgrades, saving bandwidth.

Add BroadcastChannel-based cross-tab auth sync so logging in on one
tab dismisses the login modal on all other tabs (and vice-versa for
logout).

Add a reminder to the vendored JS update script about the
version_html() regex lookahead.

* fix: remove unused import in test_web_helpers
2026-04-05 16:25:53 -07:00