* fix(ai/gemini): coerce numeric/boolean enum values to strings
Gemini Function Calling only accepts string enum values (see
https://ai.google.dev/gemini-api/docs/function-calling). Tool schemas
containing enum: [1,2,3] or enum: [true,false] were passed through
unchanged, causing Gemini to reject the entire request with 400
TYPE_STRING errors at 'function_declarations[X].parameters...enum[N]'.
Extend cleanSchemaForGeminiWithDefs to coerce enum and const values
via a shared helper (number/boolean/bigint -> string, drop null and
non-coercible entries, de-duplicate), and retype the containing schema
as 'string' since Gemini's validator only accepts enum on string-typed
schemas.
Adds seven vitest cases covering numeric/boolean enum, const, nested
schemas, and dedup/empty-after-coercion edge cases.
Fixes#104566
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ai/gemini): make enum retyping property-order independent
Address @clawsweeper review on #104567:
The initial patch set cleaned.type = 'string' inline while visiting the
'enum' or 'const' key. For schemas where 'type' appears AFTER 'enum' /
'const' in the input object — a valid ordering since JSON Schema property
order is not semantic — a later 'type' pass would overwrite the coercion
via the tail 'cleaned[key] = value' branch, and Gemini would still receive
an invalid declaration.
Move the type override out of the loop: track whether any enum/const was
coerced via a boolean flag, and force cleaned.type = 'string' after the
loop finishes. This makes the fix independent of input property order.
Also add curly braces around single-line ifs to satisfy check-lint
(eslint(curly)) which failed on the previous commit.
New reversed-order regression tests added.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ai): preserve Gemini enum schema types
---------
Co-authored-by: leonme <leonme@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): delete successful on-exit deleteAfterRun jobs
deleteAfterRun finalization was restricted to at schedules, so a successful
on-exit job with deleteAfterRun=true was retained as a disabled job instead
of being removed (the gateway exit watcher durably disables on-exit jobs
BEFORE firing as replay protection, and no later transition deleted them).
Successful delete-after-run now covers every one-shot schedule kind; the
pre-fire disable ordering is unchanged. Regression covers deletion on ok,
retention without the flag, and retention on failure through the same force
path the exit watcher fires.
Fixes#104518
* test(cron): prove durable on-exit finalization
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): lease managed worktrees across a run with an exclusive removal claim
A resumed session's managed worktree could be removed mid-run by idle GC, a
separate remover, or a manual worktrees.gc, destroying in-flight work; a session
whose worktree was already removed was admitted anyway and ran without its
checkout.
Runs now take a shared run-lease and removal takes an exclusive claim, both held
as rows in the shared state_leases table so admission and removal serialize
through one BEGIN IMMEDIATE transaction. Lease rows carry an opaque token plus
pid and process start time; dead or reused-pid owners are pruned inside the same
transaction. Removal claims before any cleanliness or snapshot work and rejects
while a live run lease exists; a run releases only its own token. The worktree id
is resolved from the session binding or by containment over both the run cwd and
workspace dir, so a workspace-only child still leases. The git worktree lock is
retained as a secondary raw-Git guard and refcounted per process so a parent and
a same-process child do not unlock each other.
* fix(agents): serialize worktree git lock cleanup
* test(agents): seed worktree races in sqlite
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(ui): redesign /btw side chat into floating multi-turn panel
The inline BTW side-result card becomes a floating Side chat panel pinned
to the thread column: multi-turn conversation with markdown answers, a
follow-up input that carries the previous side answer as context, and
header actions to clear (retires the pending run) or close (conversation
and pending run survive; an arriving answer reopens the panel).
* fix(ui): carry prior side question in follow-ups; display typed question structurally
Review follow-ups: the /btw follow-up command now embeds both the previous
side question and answer (answers alone lose the referents), and the panel
shows the user's typed question via structured state (pending record
correlated by runId) instead of parsing a marker out of user-controlled
command text.
* fix(ui): stop pending side runs on clear and keep rejected follow-up drafts
Trash now sends a targeted chat.abort for the pending /btw run (the retire
set still suppresses its late events if the abort races), the follow-up
input is hidden while disconnected like other slash sends, and a rejected
detached send restores the typed follow-up into the panel input.
* test(ui): attach side-chat panel test container for restore assertion
* fix(ui): block side-chat follow-ups while pending; notify rejection on pre-send exits
A follow-up while a question is pending would retire the in-flight run and
silently drop its answer, so the panel input is disabled until the answer
lands. The rejection callback now fires on every non-accepted path of the
guarded detached send (early exits, guard dedupe, rejected acks), not just
rejected acknowledgements.
* fix(ui): render side-chat questions verbatim
Pending and turn questions are already display-ready (chat-send strips
composer commands, the server echo carries no /btw prefix, panel follow-ups
pass structured text); re-extracting corrupted questions that begin with a
command token.
The behavior fix landed on main in #104608 (fe3884a7ad): draft-stream final
pagination now routes markdownSource previews through markdownToTelegramChunks,
the same chunker the durable reply funnel uses (delivery.replies.ts
buildChunkTextResolver), and the overflow-pagination golden was re-recorded on
safe boundaries there. This adds the missing parity regression coverage:
table-driven cases for mid-word, mid-entity, mid-tag, and fenced code-block
page boundaries assert the streamed pages equal the durable chunker's HTML
chunks exactly, and that retained/current page snapshots carry the durable
plainText projection so an HTML-parse 400 degrades both funnels identically.
Formatting proof: Testbox tbx_01kx9n9q7rksfxgr5baj0esw57 check:changed format
lane passed on this diff (local worktree has no node_modules for the hook).
* feat(ui): cron automation-ideas gallery and Create & run now in quick-create
The cron page gains a curated automation-ideas gallery (six starter
automations that pre-fill the quick-create wizard with editable prompt,
name, schedule, and delivery presets); it stays expanded until the first
job exists, then collapses to a slim band. The quick-create wizard's
final step adds Create & run now, which chains cron.run (force) on the
job id returned by cron.add so a fresh automation shows output
immediately instead of waiting for its first scheduled tick.
Also hardens scripts/control-ui-i18n.ts against models wrapping JSON
replies in Markdown code fences.
Closes#104746
* chore(ui): regenerate locale bundles for cron suggestions strings
Generated via authenticated ui:i18n:sync (claude-sonnet-4-6) for the new
cron.suggestions/quickCreate keys; keeps the locale structural-alignment
gate green.
* fix(ui): deliver watchdog suggestion results via notify preset
Codex review: the night-watch idea promised alerts but pre-selected the
isolated preset, which maps to deliveryMode none, so results were never
delivered. Use notify and reword the prompt to an explicit one-line
verdict (renamed key so the locale pipeline retranslates).
* chore(ui): regenerate locale bundles for watchdog suggestion
* chore(ui): refresh raw-copy baseline after rebase
* fix(mattermost): bound stalled inbound media header waits
* test(mattermost): always close stalled media server
* test(mattermost): satisfy server cleanup lint
---------
Co-authored-by: NIO <nocodet@mail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* docs(gateway): add launchd supervisor loop troubleshooting
* docs(gateway): warn before disabling node LaunchAgent
* chore: trigger CI after rebase onto main
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(gateway): modernize launchd recovery runbook
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): refresh model picker component config
* test(discord): model full hot-reload snapshot state
* fix(discord): keep pending model selection stable
* fix(discord): stabilize recent model actions
---------
Co-authored-by: compoodment <compoodment@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): keep failover detail truncation UTF-16 safe
Replace raw .slice(0, MAX_FAILOVER_DETAIL_CHARS) with truncateUtf16Safe
so provider error details containing emoji or other non-BMP characters
are not split at a surrogate-pair boundary before failover classification.
Adds a regression test via extractFailoverSignalDetails.
* test(agents): tighten failover UTF-16 coverage
---------
Co-authored-by: chengzhichao-xydt <chengzhichao-xydt@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
On rare page loads (seeded, ~12%, once per load) the sidebar pet's first
scheduled visit perches in the brand-logo slot instead of the footer
ledge: the static mark crossfades out and the live sprite (palette,
build, accessory, blink) settles in, then departs and the logo returns.
One crab, two homes - never both at once. Offline/busy recalls the
stand-in to ledge duty; rides the existing visit scheduler and the
lobsterPetVisits setting, so no new config surface. Reduced motion keeps
the swap static.