Both slices extend the edit plan request, the video operation imports,
the edit_video contract imports and the media card E2E; the conflicts
are unions of both sides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
Follows the repository's migration convention: the constraint commits
without scanning media_jobs, and a second migration validates it with a
lock that lets reads and writes continue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
A job that recovers after a crash runs again and reports the same
skipped platforms; the newest reason per platform now replaces the
earlier one instead of listing it twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
To cut a social video well the bot has to know where the cuts fit, and it
could not look at a video at all. analyze_video is a read-only media job:
one ffmpeg pass prints scene changes (scene score above 0.3, one per cut),
pauses (silencedetect with the remove_silence defaults) and short-term
loudness (ebur128) to files in the job directory, and a transcript made
from the video by transcribe (an SRT whose parent is the video) adds when
each line is spoken. The job stores nothing; its card just says done.
The result reaches the bot through the existing media job path: the job
always wakes the bot that started it (the tool has no continue_after) and
the summary carries a short legend plus compact JSON. The runtime's
summary limit grows to 12,000 characters and the summary is fitted into
it: at most 200 entries per list, then every list shrinks evenly, keeping
the strongest cuts, the longest pauses and the start of the transcript,
with omitted counts. The wake prompt no longer claims the card shows the
result when a job made no files.
Jobs read derived artifacts through a new fetchDerivedArtifact, scoped to
the job's user like fetchArtifact. A migration admits the operation in the
media_jobs check and adds the tool to existing Studio specialists that
still edit video. The web card gets an icon; no new copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
The card's Export menu now lists the platforms first, by brand name and
untranslated, then the aspect ratios after a separator; the platform an
output was made for is left out, like its own shape. A platform export
shows the platform's name under the player, or, when its check did not
pass, one line for the first issue instead ("Too long for TikTok (max
10:00)"), in the warning or destructive color. Skipped platforms get
the same kind of line, and they stand in for the error of a job that
skipped every platform, so the card does not say the same thing twice
in two languages.
The E2E spec seeds a platform export with a failed check and a skipped
platform, and captures the card and the open menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
Talking-head clips are mostly cut by hand to drop pauses, and the bot had
no way to find them: it could only keep segments it guessed. The new
remove_silence step finds every pause quieter than threshold_db (default
-35 dB) and at least min_silence long (default 0.6 s), keeps padding
(default 0.12 s) on each side so word onsets survive, and cuts the rest
through the existing keep-segments graph, so picture and sound stay in
sync.
Steps run in order, so silence is detected on the timeline the steps
before it leave: the planner gets an analysis mode that sends that audio
through silencedetect into a null output, and ametadata prints the marks
to a file in the job directory. The detected speech is handed back to the
planner per step. Nothing silent makes the step a no-op; nothing left
fails with a clear error. At most 200 pieces are kept, bridging the
shortest pauses first, so the filter graph stays a sensible size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
Exports only knew aspect ratios, so a cut could still be rejected or
degraded on upload: too long for the platform, over its size limit, or
far off its loudness. export_video now takes platforms (TikTok,
Instagram Reels and Feed, YouTube Shorts and YouTube, Facebook Reels,
LinkedIn, X, Pinterest, Snapchat Spotlight), up to six targets with the
presets. Each platform is rendered to its frame with the export's fit,
its frame rate cap, a bitrate cap that also keeps the file under the
size limit, and its loudness target in one loudnorm pass.
Every render is probed and measured afterwards. A file over the size
limit is encoded once more at a bitrate scaled from what it averaged,
and audio more than 2 LU off the target gets one linear loudness pass.
The result is stored on the output as a pass, warn or fail check with
language-neutral issue codes. A platform the video is too long or too
short for is not rendered: the card lists it with the reason, the
other targets still render, and the bot is told to trim first.
The limits come from the earlier profile research, with its sources,
review date and unverified flag; content, disclosure and caption rules
are left out because they need inference or a review flow. Preset
definitions move to their own module so the platform schemas can use
their aspect ratios without an import cycle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
Without a model key the settings opened on the keyless deployment default
(OpenRouter). The connect flow now opens on Claude Opus 5, so a new user
only adds a key or signs in. A connected credential, an owner-chosen
default, or a deployment key still decides as before.
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD
Co-authored-by: Claude <noreply@anthropic.com>
Disable build-push-action build record uploads in publish-server-image;
they are large and unused by the digest publish flow. Digest artifacts
already expire after one day. Set seven-day retention on desktop e2e
screenshots so CI debug uploads do not inherit the org default.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
Remove unmounts itself once clicked, so focus fell to the body and the
settings dialog pulled it back to its popup a frame later. Whatever field
the user (or a test) focused in that frame lost focus again; in CI the
cleared transcription URL was still sent on the next save. Remove now
moves focus to the key field before it disappears.
Claude-Session: https://claude.ai/code/session_01UfVneQG6488JGLiHYAc9RZ
Co-authored-by: Claude <noreply@anthropic.com>
The attach sheet's autopilot now offers "Until a time" with the native
time picker from Expo UI: Android's clock dialog, an iOS wheel in a sheet
with Start. The home list opens with the autopilots that run, wait or
ended within a day, as Today does on web, and each row opens its thread.
The attach and until menus move to the shared action sheet, so Android
pages them instead of dropping buttons past three.
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1
Co-authored-by: Claude <noreply@anthropic.com>
The autopilot's start line counts its steps and folds open to the latest
checkpoints, and its result reads as a card. The composer's plus menu
starts an autopilot with the draft as the order and a choice of until
done, tomorrow morning or a time. Today lists the autopilots that run,
wait or ended within a day, and their step runs no longer show one by
one. Autopilot runs send no completion or failure push; only a pause
notifies.
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1
Co-authored-by: Claude <noreply@anthropic.com>
update_bot takes a look (oo1: value), validated and normalized in core.
A suggested look renders as a "New look" card with the avatar preview on
web and mobile; accepting applies it to the bot.
Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj
Co-authored-by: Claude <noreply@anthropic.com>
An autopilot per thread continues the user's order run after run, with
no time limit. Each run reports a checkpoint; after three runs without
progress or three failed runs it pauses with Continue and Stop. Start,
pause, stop and finish show as quiet lines in the conversation.
Approvals during an autopilot run are parked as suggestions, and the
temporary approval rules end with the autopilot.
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1
Co-authored-by: Claude <noreply@anthropic.com>
The Atelier replaces Avatar Studio: pick a species, colour, eyes,
glasses, hats, neckwear, arms and temperament, with every option drawn
as your own Oo wearing it. Onboarding's "Design your Oo" now offers
species looks per vibe and opens the Atelier for the rest; mobile bot
settings carry the same editor instead of a bare colour row, so picking
a colour there no longer throws away the rest of the look.
Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
Bots can offer an exact tool call as a card. Nothing runs until the user
taps Accept; the call then runs as its own approved run of the bot it was
meant for, and the card shows the outcome. Open suggestions also wait in
the inbox and expire after a week.
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1
Co-authored-by: Claude <noreply@anthropic.com>
A bot's Oo now cheers for a moment when its run finishes, the stage Oo
follows the pointer and squishes when tapped, and chat headers show the
speaking bot's own Oo instead of the generic mascot. Small avatars in
lists only animate while working or waiting so long lists stay calm.
Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj
Co-authored-by: Claude <noreply@anthropic.com>
Opening the web app over the Mac's Wi-Fi address (an insecure context)
crashed Connect because crypto.randomUUID is unavailable there, and the
MCP OAuth redirect, Composio/Pipedream return and noVNC link all pointed
at WEB_ORIGIN (127.0.0.1). The API now derives these URLs from the
request's browser origin when it is loopback or a private network, the
same hosts CORS already trusts, and falls back to WEB_ORIGIN otherwise.
Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD
Co-authored-by: Claude <noreply@anthropic.com>
Native avatars now draw the shared Oo rig with react-native-svg instead
of a visor with two equal eyes, so the Oo eyes look the same on every
surface. The robot/organic avatar setting is gone from web and mobile
because every bot is now an Oo; uploaded pictures show on native too.
Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj
Co-authored-by: Claude <noreply@anthropic.com>
Bots now render as an Oo rig built once in core: nine species, styled
Oo eyes, eyewear, headwear, neckwear, arms and moment props, with a
level of detail per size. A shared animation frame loop moves only
visible avatars and holds still under reduced motion.
Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj
Co-authored-by: Claude <noreply@anthropic.com>
* feat: show routines the bot sets up from chat as one quiet line
When someone mentions something recurring in passing ("every Sunday I plan the
week"), the bot now creates the routine right away and the conversation shows a
single line with the schedule, Edit and Undo, on web and mobile.
- New `event` message block for routine lines, kept in history so later turns
know what was set up or removed.
- schedule_create reuses an active repeating routine of the same name instead
of adding a duplicate, caps a bot at 50 active schedules, and falls back to
default times when none was given.
- Removing a routine (Undo, the editor, or schedule_cancel) marks its lines
removed via the new routines.noticeMessageId column.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1
* test: give the routine notice test a complete job publisher
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1
---------
Co-authored-by: Claude <noreply@anthropic.com>
A bot's look is stored in its `color` string, which today only holds a
colour, an optional classic shape index or an uploaded picture. The
customizable avatars (species, eye styling, accessories, temperament)
need a richer value that still travels everywhere `color` already goes:
bot rows, group members, mentions and the inbox.
- `@rakazo/core` gains `oo-avatar`: an `oo1:` key=value codec that writes
only non-default fields, reads every legacy value unchanged, rejects
malformed values, and exposes `ooAvatarColor` for plain CSS consumers.
- The rig's eye anchors keep the Oo trademark (big eye = small eye × φ),
and lenses scale with their eye so glasses stay asymmetric; the shared
asymmetry ratio now comes from core.
- The API rejects malformed `oo1:` values on bot create and update.
- `parseBotAvatar` reads through the codec, so an `oo1:` value already
renders with its colour and shape before the new renderer lands.
- The agent cursor and the mobile avatar used the raw value as a CSS
colour and broke on `#hex::shape_N`; they now use the decoded colour.
Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj
Co-authored-by: Claude <noreply@anthropic.com>
Signing up refreshes the session, which briefly routes /onboarding to
/sign-in without a return path; the sign-in route then sent the new
account to /app. The shell's own redirect to onboarding was skipped
whenever a focus refresh landed before the bootstrap response, so a
fresh account could stay on an empty Today view.
/onboarding now sends signed-out visitors to /sign-in?next=/onboarding
and sign-in honours it, and the shell redirects an account without
any bots or conversations regardless of which refresh landed first.
Without a connected model nothing can reply, so the composer shows a
"Connect model" action that opens model settings instead of a send
error after the fact.
Claude-Session: https://claude.ai/code/session_019HKhzQ65ctE5qmkiF9Apxo
Co-authored-by: Claude <noreply@anthropic.com>
.env.example ships RAKAZO_COMPUTER_MEMORY, _CPUS and _PIDS_LIMIT with no
value. Loaded as empty strings, they bypassed the ?? defaults and the
startup validation threw, so the supervisor crashed on a fresh setup.
Claude-Session: https://claude.ai/code/session_012UBam2saiShpLy5o2cy3h8
Co-authored-by: Claude <noreply@anthropic.com>
The centered Create new Bot modal was rendered inside the side panel's
condition, so the empty states that open it (nothing selected, or a
project without bots) only showed it while a panel happened to be open.
It is a portal to the document body, so it now renders after the panel.
- The project drag grip's accessible name goes through Lingui (German
"Ziehen zum Umsortieren") instead of a hard-coded English label.
- Drop the project drag state that was set but never read, and imports
Shell.tsx no longer uses.
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
Co-authored-by: Claude <noreply@anthropic.com>
* chore: remove legacy orchestrator, trading and one-off script leftovers
The repository still carried the pre-evOo prototype: a Python orchestrator,
the old HTML "evoo-platform" cockpit (whose test ran in the root unit
suite), an apex-trading package, the hot-reload deploy.sh for it, a
permissions manifest, an architecture note with local file links, agent
handoff notes, a Steve Jobs persona template and about ninety one-off
scripts that probe a developer database with hard-coded local URLs. None
of it is used by the product, CI or the real deploy in ops/evo-deploy.
Delete those files, drop the evoo-platform test glob from the root vitest
config, the Cockpit section from .env.example and the dead biome and
gitignore entries, and update the docs that listed the removed tools.
scripts/start-tmux.sh no longer starts the removed sentinel and resolves
the checkout from its own location instead of a machine-specific path;
scripts/seed-ollama.ts requires DATABASE_URL instead of falling back to a
local URL with a username.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): add media contracts, schema and provider interfaces
Groundwork for image and video generation, video editing and transcription.
Nothing calls it yet; the media runner, providers, tools and UI build on it.
- Contracts: video, audio and subtitle MIME types, per-kind size caps for
streamed and generated artifacts, video/audio message blocks and the live
media card block, media operations, job statuses and output presets
(9:16, 16:9, 1:1, 4:5, 4:3, 21:9).
- Core: attachment kinds include video and audio, artifacts become video or
audio blocks, and agent history keeps media artifact ids so later turns
can refer to them.
- Schema: artifacts record width, height, duration and the artifact they
were made from; media_jobs tracks generations, edits and transcriptions.
- Adapter kit: provider-neutral image generation, video generation and
transcription interfaces, the media job context operations run in, and
the MediaRuntime that tools use to start jobs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): drop trading and persona prompts from the conversation shell
The web shell still showed leftovers from the trading prototype: composer
chips with German trading and "Steve Jobs" prompts, trading suggestion
chips under the empty conversation line, a German trading hero for the
empty transcript, and a German "data protection" status panel in the
sidebar. They suggested actions the product does not have and added
persistent chrome.
Remove the composer chips and their helper, the empty-state chips and the
hero (the empty conversation keeps its existing one-line start hint), the
sidebar trading indicator and the data trust panel. The new-bot E2E now
asserts the plain empty state; the sidebar spec no longer expects the
trading status, and the placeholder Steve Jobs spec with no assertions is
gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor: remove the trading bot status integration
With the sidebar trading indicator gone, the only remaining consumer of
the trading bot status was a banner in General settings that reused the
same component. evOo is a personal assistant; a status proxy and poller
for one external trading service, configured through service-specific
TRADING_BOT_URL and VITE_TRADING_BOT_URL variables, does not belong in
the core product.
Remove the settings banner and its prop plumbing, the web poller, the
indicator component, the /api/trading-bot/status proxy, the shared core
contract, the env loading and both variables from .env.example, together
with their tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore: remove one-off database scripts and a stale review note
packages/db carried four hand-run scripts outside the package build that
query or seed a developer database with a hard-coded connection string,
one of them for a specific account. docs/VOICE-FIRST-REVIEW.md was a
one-time verification note for a workflow that no longer exists and
named private machines and local paths. Nothing references them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(memory): remember appends dated notes instead of replacing memory
The remember tool rewrote the bot's whole MEMORY.md with each call, so every
new fact erased the earlier ones unless the model re-sent the full document.
- MemoryCommitRequest gains mode "append" (the store joins the note onto the
current content inside the same serializable transaction and still writes a
revision row) and every commit is capped at 16 KB per document.
- remember appends a dated bullet ("- YYYY-MM-DD: ...", the user's local date)
by default and rewrites only with mode "replace"; a full document returns an
error that tells the bot to condense it. Retries of the same tool call stay
single appends through the existing effect record.
- Scripted runtime memory entries can append too; the scripted "remember"
script now appends instead of overwriting the bot's memory.
- When memory does not fit the run's 32 KB budget, the prompt keeps a
document's newest lines instead of its oldest.
- memory.update maps an oversized save to BAD_REQUEST.
- docs/PRODUCT.md records that UserFact is the cross-space user model and the
space MEMORY.md stays space notes (no migration).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(coach): goals, habits, check-ins, daily briefs and check-in rhythm
The main agent should coach the user, but nothing stored what the user
wants to achieve or keep doing, and no routine ever checked in. This adds
the data model and the interfaces the onboarding intake and the coach
tools build on.
- Schema: UserGoal, Habit, CheckIn (one per habit, goal or day and date)
and DailyBrief, all keyed to the user's local calendar date, plus a
coachKey on Routine that marks the main bot's coach check-ins.
- Contracts (coach.ts): CoachRhythm, Goal, Habit, CheckIn, DailyBrief and
CoachToday DTOs and the RPC inputs.
- Repository (@rakazo/db coach.ts): goal, habit and check-in CRUD with
per-user limits, upsertDailyBrief and loadCoachToday with streaks in
the user's time zone.
- coach-rhythm.ts: setCoachRhythm/getCoachRhythm keep up to three main
bot routines (morning brief, evening check-in, weekly review) in the
main conversation in step with the user's chosen times.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(notifications): add push subscriptions, notification settings and web push interfaces
Push could only reach one mobile device per user (a token file), quiet hours did not exist,
and the web app had no service worker, so a closed tab never heard about finished work.
This lands the shared interfaces first so onboarding can build on them:
- push_subscriptions (web push endpoints and Expo tokens, one row per device) and
user_notification_settings (quiet hours as minutes in the profile time zone, nudges, a
per-day nudge counter), with CHECK constraints.
- NotificationSettingsSchema and web push contracts, notifications.settings.get/update and
notifications.webPush.config/subscribe/unsubscribe (owner-scoped, https endpoints only).
- getNotificationSettings/updateNotificationSettings plus the policy and nudge-cap helpers.
- VAPID keys from VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY or generated once and stored encrypted,
and an SSRF-guarded web push sender built on the web-push package.
- The web app links its manifest, registers a push-only service worker where supported (secure
context, not the desktop app) and exposes webPushSupported/webPushEnabled/enableWebPush/
disableWebPush.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore: replace machine-specific paths and addresses with placeholders
ORIGIN.md pinned the intended checkout to an absolute path under a
personal home directory, docs/LOCAL-SETUP.md listed folders on one
developer's machine, and two tests used what looks like a real tailnet
address. Use a home-relative path, drop the machine-specific folder list,
and use 100.100.100.100 in the tests; it is still inside 100.64.0.0/10,
so both tests check the same thing as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(coach): repository, streak and rhythm suites
Covers what the coach interfaces promise before anything builds on them:
goal and habit ownership and limits, one check-in per habit, goal and day
dated in the user's time zone, streaks across weekends and weeks, and a
rhythm that keeps exactly one main-bot routine per check-in and is
idempotent when set again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(contracts): share one clock-time schema
The coach rhythm and the quiet hours both parse "HH:MM" wall-clock times
and each defined its own ClockTimeSchema, which the contracts index cannot
export twice. Both now import it from one module.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): ask the onboarding questions in the main conversation
The wizard asked for the account name again and a "Tell us about yourself" form, and the
focus answer in the main conversation was never saved. The main bot now gets to know the
user in the main conversation instead, one card after each answer.
- UserOnboarding keeps the pending step, the focus and when onboarding finished.
- onboarding-intake.ts declares the steps (focus, goal, rhythm, quiet hours, notifications)
as cards plus what their answers change; onboarding.ts commits an answer, the focus app
suggestions and the next card or the closing line in one transaction.
- The choice block gains a step, an optional text input and the typed answer; choose takes
the step, the text and whether the device can still turn push on.
- The focus answer becomes a goals fact; the goal, rhythm and quiet-hours effects and the
push seam are placeholders until the coach and notification modules are merged.
- The wizard drops the preferences step and fills time zone, locale and language from the
browser; the main bot's prompt notes open onboarding cards; agent history shows a card's
question and answer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): transcribe speech on any OpenAI-compatible server and write subtitles
Transcription has to run on the user's own hardware, and the only speech-to-text
path so far was the OpenAI voice key. Self-hosted whisper servers (whisper.cpp,
faster-whisper based servers, vLLM) all speak the OpenAI transcriptions
protocol, so one provider covers them.
- createOpenAiCompatibleTranscriptionProvider posts a multipart upload to
{base}/audio/transcriptions asking for timed segments, falls back to plain
JSON for servers that reject verbose_json, reads text/plain bodies, and
reports failures in words. It reuses the address-checked fetch and URL
guards of the other OpenAI-compatible adapters, caps uploads at 25 MiB and
responses at 8 MiB, and builds the multipart body by hand because the
undici fetch behind the address checks does not serialize the global
FormData class.
- An in-process server emulator (OpenAI/whisper.cpp, plain-JSON-only and
text-only flavours) and an in-process provider back an offline
conformance suite, plus one loopback test through the real fetch.
- toSrt/toVtt in core turn segments into SubRip and WebVTT, wrapping long
segments into cues of at most two 42-character lines that share the
segment's time, and escaping WebVTT cue text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(notifications): fan push out to every device, honour quiet hours, add notify_user
Run notifications reached at most one phone and nothing could hold them back at night.
- PushNotificationProvider sends each notification to all of the user's registered devices
(web push and Expo), skips everything during quiet hours in the profile time zone, drops
devices the push service reports gone (404/410, DeviceNotRegistered), records lastUsedAt and
links each push to its conversation.
- Expo tokens move from DATA_DIR/push-tokens files into push_subscriptions: the API imports and
deletes the legacy files at start; notifications.registerPush/unregisterPush keep their
contract for the mobile app (tokens must look like Expo tokens).
- NotificationMessage gains the nudge kind, and the main bot gets notify_user: a short push to
the main conversation that respects the nudge switch, quiet hours and a cap of three per day.
It is withheld from shared messaging channels and from runs answering another owner's agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(coach): main-bot coach tools and goals in its prompt
The main agent had no way to record what the user wants to achieve or how
their day went, and its prompt never showed their goals or habits.
- update_goal, update_habit, log_check_in and write_daily_brief, main-bot
only and, like update_user_profile, withheld from shared channels and
from runs that answer another owner's agent. Goals and habits can be
named by id or exact title.
- The main bot's own runs list active goals and habits (with ids, done
state and streak) in the <user> block; the basics-only block and every
other bot never see them. The main agent role adds a short coaching
stance and names each coach tool only when the run has it, so the
check-in routine prompts stay tool-agnostic.
- The scripted runtime understands a few coach commands and the morning
check-in, so tests can drive the tools end to end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): add the Studio specialist the main bot hands media work to
Media work (cutting for a format, subtitles, exports) needs editing judgement
and a focused tool set, so the main bot should be able to delegate it to a
visible specialist instead of doing it itself.
- ensureStudioBot creates "Studio" once per user, next to the main bot so it
can join the main conversation and read its media, tagged media, with
editor instructions (hook first, tight pacing, 9:16 safe areas, subtitles
for muted playback) and a restricted builtin allowlist: every media
operation plus files, questions, memory and web lookup. It has a thread,
so the main bot's specialist directory lists it. The spawn key carries the
user id so members of a shared space each get their own.
- A Studio the user archived or deleted stays gone. Deleted bots now record
their spawn key in bot_deletions so idempotent creation can tell.
- Postgres suite for concurrency, shared spaces, archive and delete.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): use English Lingui sources for German UI text
Several web surfaces hard-coded German (the project card, roster and
activity status labels, the sidebar "Heute" label) or used German text as
the Lingui source message (sidebar sections, project timeline, bot picker,
voice entry, group stage, copy and read-aloud buttons). English users saw
German, and because production builds keep only message ids, none of these
strings could be translated per locale.
Convert them to English source messages, keeping the meaning:
- ProjectCreatedCard: Lingui Trans/Plural instead of literals.
- agent-activity: labels go through i18n._ with literal ids so the module
stays importable from plain unit tests; the extractor still collects them.
- Shell, ProjectSessionTimeline, bot-picker, StageVoiceEntry, OoStage:
English sources, reusing existing messages where one already fit
("just now", "{n}m ago", "Search", "No bots match", "Set up voice",
"Group conversation", "Transcript").
- The conversation picker matches its create entry against the translated
label instead of a fixed German string.
The German wording returns through the de catalog in the next commit.
Unit and E2E tests now assert the English labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): keep Lingui catalogs complete and in sync
The committed catalogs were last extracted weeks ago: about 330 source
messages were missing from every locale, and production builds keep only
message ids, so those strings rendered as hash ids (sidebar sections,
privacy settings, profile, timeline, ...). German also had 226 empty
entries, and every extraction rewrote the files because references carried
line numbers.
- lingui.config.ts: po references without line numbers, and a stable sort
that breaks Lingui's ordering ties (explicit ids, equal text) by id, so
the output no longer depends on file-system order.
- Re-extract all catalogs with --clean (drops 25-44 obsolete entries per
locale; no existing translation changed).
- Translate every German entry (du form, consistent with the existing
catalog). Other locales keep empty entries and fall back to English.
- i18n-catalog-sync.test.ts extracts into a copy of the catalogs and fails
when the committed files differ or a German entry is empty, so stale
catalogs fail the unit test job (offline, about 5 s).
Adds @lingui/format-po (already installed through @lingui/cli) as a direct
dev dependency to configure the formatter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): add the ComfyUI provider, default workflows and emulator
Image and video generation run on the user's own GPU server. ComfyUI is the
common self-hosted backend, so this adds a provider for its HTTP API that the
generation operations can call.
- Core: API-format workflow templates with a small placeholder format
({{prompt}}, {{width}}, {{seed}}, ...) that is validated and filled without
evaluating anything. Defaults: text-to-image and image-to-image on core nodes
for any SD-family checkpoint, and text/image-to-video on ComfyUI's core
LTX-Video nodes; a README lists the placeholders and model files.
- Adapters: createComfyUiProvider submits a workflow, polls its history,
uploads input images and downloads outputs through the address-checked fetch
used for other self-hosted model servers, with timeouts, capped reads and
readable errors. A custom workflow from settings replaces the default it
matches. Cancelling dequeues or interrupts only its own prompt. GPU status
counts memory ComfyUI can free for itself as available.
- Contracts: input schemas for generate_image, generate_video and edit_image.
- Tests: an in-process ComfyUI emulator, a provider-neutral generation
conformance suite and provider tests, all offline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(memory): derive the run's memory budget from the document limit
The prompt budget was a separate 32 KB constant; it is now twice the
per-document limit, so a full bot document and a full space document keep
fitting if either number changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(profile): edit facts in place in About you
About you could only confirm or delete what the bots know about the user, so a
fact that was slightly wrong or filed under the wrong category had to be
deleted and taught again, and a fact could not be limited to some spaces from
the app even though the profile supports it.
- Each fact gets an Edit button that turns its row into an inline editor:
wording, category, and (when the user has more than one space, or the fact
is already limited) which spaces it shows in. Escape or Cancel leaves it
unchanged; only changed fields are sent through profile.update editFacts.
- A limited fact names its spaces next to its source.
- The repository validates edits like new facts: a wording whose normalized
form is empty or equals another fact of the user is refused, all edits of
one update apply atomically, and the stored embedding is only dropped when
the wording really changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(inbox): list and answer items from all of the user's spaces
The inbox in the main conversation only showed asks, approvals, takeovers and
unread threads of the current space. The main conversation lives in the home
space, so anything a bot needed in a project space never reached it.
- inbox.list now covers every space the user belongs to (except spaces being
deleted), still only the user's own bots, runs and threads. Items from
another space carry that space's id and name.
- The web inbox shows the space name next to the bot for those items, answers
them with that space's header so the existing thread procedures check
membership and ownership for that space, and opens their thread in it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(coach): coach RPCs for goals, habits, check-ins, today and rhythm
The web app needs to show and change the user's coach data, and the
check-in times need a way to change after onboarding.
- coach.today, coach.goals.*, coach.habits.*, coach.checkIns.* and
coach.rhythm.get/set, owner-only and not space-scoped; another user's
ids read as not found, invalid input as a bad request.
- coach-rhythm reuses the shared clock-time helpers.
- Tests: an API suite covering ownership and validation for every
procedure, the coach tools driven through the main bot (and refused
for a specialist), and a morning check-in routine that fires in the
main conversation and writes today's brief; the unauthenticated
authorization sweep lists the new procedures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): save goal, check-in rhythm, quiet hours and push from the cards
With the coach and notification modules merged, the onboarding answers now take effect:
- the goal card creates a goal (source onboarding) through createGoal;
- the rhythm card sets the main bot's check-in routines through setCoachRhythm, and the
closing line names the first check-in from the routines that were saved;
- the quiet-hours card updates the notification settings;
- the choice card tells the server whether this browser can still turn push on, so the
notifications card is offered only there, and "Turn on" subscribes before answering.
The thread lock becomes NO KEY UPDATE so a rhythm routine, which references the thread, can
be written while the answer holds the lock. The goal answer is capped at the coach title
length. A Postgres suite walks the questions end to end: answers, skips, a dismissed card, a
stale and a doubled answer, another bot's focus card, and another user's access.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): stream artifacts over HTTP with Range support
Videos and audio can be hundreds of megabytes. Artifacts only travelled as
base64 inside RPC calls, capped at 10 MiB and held whole in memory, so media
could neither be uploaded, played with seeking nor produced by media jobs.
- ArtifactStore gains stat, createReadStream (inclusive byte ranges),
putStream (size cap enforced while writing, real SHA-256) and an optional
localPath so ffmpeg can read stored files in place. LocalArtifactStore
implements them with the same safety as before (O_EXCL, O_NOFOLLOW, 0600)
and now validates space ids and keys before building paths.
- storeArtifactStream / storeArtifactFromFile store a stream or a job's
output file as an artifact row, with width, height, duration and parent,
and remove the blob again if the row cannot be written.
- GET /api/artifacts/:id serves an artifact with Range/206/416, ETag,
inline disposition for media and attachment otherwise (safe RFC 6266
filenames), nosniff and same-origin CORP. Authorization matches
artifacts.get: the owner's artifact of an active bot thread or group,
with membership of the artifact's space (media elements cannot send the
space header; an explicit header must name the same space).
- POST /api/artifacts takes a raw streamed body with the target and name in
the query, authorized like artifacts.create, with the MIME allowlist and
the per-kind caps checked from Content-Length and again while writing.
- The API CSP allows media from the app origin and blob URLs.
- Web: artifactUrl(artifactId) and uploadArtifactFile for the UI.
Tests cover the streaming store, range parsing, filename sanitizing, the
file helper, and a Postgres suite for both routes: ranges, caching, other
users, shared-space members, other spaces, archived threads, signed-out
requests and upload limits including chunked bodies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): media job runtime, media queue and media settings
Media operations (generation, edits, transcription) need a runner that works
outside the agent turn, keeps a card in the conversation current and never
lets two processes put work on one GPU. Operations land in later commits; this
is the runtime they register with.
- Background job `media.run { jobId }` on a named `media` queue. Jobs carry an
optional queue: the Graphile publisher passes it as queueName and the
in-memory driver runs each queue one job at a time, in order.
- `createMediaRuntime` implements MediaRuntime (capabilities, start, get,
cancel) over a list of `MediaOperationRegistration`s and runs jobs:
start validates the input, creates the job and posts the bot's media card in
one transaction (idempotent per tool call through an operation key), then
enqueues it. The runner claims a fenced lease (an advisory lock keeps one
live lease at a time, so the GPU stays single even when the queue is
bypassed), renews it while polling for cancellation, writes progress and
outputs into the card, defers with backoff when the GPU is short of memory
or the operation asks to wait, bounds deferrals, crash restarts and run
time, always cleans the job's temp directory and always ends the card in a
terminal state. With continue_after the starting bot is woken in the same
transaction that ends the job (new run trigger `media`).
- The reconciler re-enqueues jobs whose enqueue was lost, whose runner died
or whose wait is over; a job stuck behind a dead worker's queue lock runs
outside the queue.
- Media backends are deployment-wide settings stored encrypted as the
`media` IntegrationProviderConfig row: `loadMediaSettings` and
`saveMediaSettings` with the same address policy as OpenAI-compatible
servers (no metadata hosts, public hosts only when allowed, HTTPS when a key
is sent), a saved key never follows the settings to another server, and the
view never returns keys. Contract in contracts/media-settings.ts.
- `createMediaOperations` is the one list both the API and the worker build
their runtime from (empty until the operations land), and media-tools.ts has
the shared `continue_after` schema helper for the tool definitions.
- Migration: lease, deferral, operation key and continueAfter columns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(notifications): cover quiet hours, delivery, nudges and the notification RPCs
The quiet-hours CHECK accepted a row with only one bound set, because a NULL comparison made
the whole condition NULL; it now requires both bounds explicitly. The migration has not shipped
yet, so it is corrected in place.
- Unit: quiet-hours math across midnight, time zones and daylight saving; VAPID key handling
(configured, generated once and shared, invalid); the web push sender (encrypted VAPID-signed
request, gone on 404/410, no private or non-https endpoints); the fan-out with fake senders.
- PostgreSQL: settings and subscription repositories (constraints, per-user removal, endpoint
hand-over, device cap, concurrent nudge cap, cascade on account deletion), delivery and
notify_user against a real main conversation, and every notification RPC through the API
including cross-user isolation and endpoint validation.
- .env.example documents the optional VAPID keys and VAPID_SUBJECT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): notification settings, and sign-out stops this browser's push
Users had no way to turn on push for a browser, set quiet hours or opt out of nudges.
Settings gains a compact Notifications section: a switch for push on this device (only where
web push works: a secure context outside the desktop app), quiet hours with from/to times
(22:00-07:00 when first turned on) and a switch for nudges. Changes save in order as they are
made. A browser that blocked notifications says so instead of offering a dead switch. Logging
out unregisters the browser first, so a shared computer stops receiving the account's pushes.
The E2E stubs the Push API (headless Chromium has no push service), walks all three controls,
checks they survive a reload and captures the notification-settings screenshot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): transcription input, chunked transcripts and the Studio trigger
Groundwork for the transcribe operation and for creating Studio when it is
first needed.
- Contracts: TranscribeInputSchema (artifact id, optional language code).
- transcribeChunks uploads audio chunks in order and stitches them into one
transcript on the original timeline; transcriptionChunkSeconds sizes
16 kHz mono WAV chunks below the server's upload limit; transcriptSummary
hands the bot the transcript, cut at a word.
- ensureStudioWhenMediaConfigured creates Studio once a media server
(generation or transcription) is configured. Local edits alone do not
count, because ffmpeg and image tools are present on most installs and a
specialist nobody asked for would appear for everyone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): show media jobs as a live card in the conversation
Media jobs report progress and results through the media block, which the
web app did not render.
- MediaCard renders the block: operation icon, title, status (reusing the
task card's labels, plus "Waiting for GPU" for jobs queued behind the
GPU), a progress bar while active, the error when failed, and outputs:
images as thumbnails with the existing lightbox, video and audio in the
media player, subtitle and text files as file cards. Finished video and
audio get a download link, "Export" with presets labelled by aspect ratio
(the video's own shape left out) and "Transcribe", offered only when the
space can run them; running jobs get "Cancel".
- MessageView renders it with one branch.
- An E2E seeds the card on a thread snapshot with real uploaded artifacts.
- artifact-url.ts and MediaPlayer.tsx are minimal stand-ins for the
pipeline workstream's files and give way to them on merge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): edit and export video with ffmpeg
Bots can now cut, reframe and export videos for every common format without
a hosted service: ffmpeg and ffprobe from PATH do the work on the server.
- ffmpeg wrapper: PATH detection, ffprobe JSON probing (rotation, pixel
aspect, frame rate, audio, packet scan when the container has no
duration), runs with -progress parsing, abort and stall kills, an output
size cap, partial-output cleanup and short errors without temp paths.
Inputs go through the file protocol with the demuxer forced from the MIME
type, so a crafted upload cannot be probed as a playlist that reads other
files or URLs. extractAudio (16 kHz mono WAV) and renderPoster for
transcription and media cards.
- A pure planner turns edit steps (trim, keep, concat, reframe, speed,
volume, mute, subtitles, extract_audio, poster) into one filter_complex
pass: leading cuts seek the input, appended clips are normalized to the
frame, fps and 48 kHz stereo (silence where a clip has none), reframes
crop, pad or blur to the presets, speed chains atempo, subtitles burn in
from generated file names with a readable style that stays clear of the
controls on 9:16 frames. H.264/AAC MP4 with yuv420p and faststart;
location metadata is stripped.
- edit_video and export_video operations and tool definitions; the inputs
are shared contracts so the media card can start exports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): stream job inputs and outputs, cover the job lifecycle on Postgres
Media files can be hundreds of megabytes, so the runner no longer buffers them:
inputs are cloned from the local artifact file when the store has one and
streamed otherwise, outputs go through storeArtifactFromFile (per-kind size cap
and MIME check while streaming; an oversized result fails the job with a short
card error).
A Postgres suite runs the runtime with fake operations: success with outputs,
posters and parent versions; input, availability, artifact and thread checks
before a job exists; tool-call replay; failure with temp-dir cleanup; outputs
outside the temp dir and sources of another user; deferral with backoff and its
budget; the GPU memory guard; cancel of a queued job and of a job running in
another process; one job per lease and one job on the GPU at a time; crash
recovery and its restart limit; reconciliation inside and outside the queue;
the continue_after wake; a cleared conversation; the run time limit; and
capabilities.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(profile): keep the fact editor's Save label visible
Small buttons in ui-web merge their text-ui-label size over the default
variant's text color, so the small Save button rendered ink on ink. The
editor now uses default-size controls, with the category select at the same
height.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): generate images and video on ComfyUI and edit images
Bots can now make images and short clips on the user's own GPU server and
rework images locally, through the media job runtime.
- generate_image: prompt, aspect preset or exact size, up to 4 images, optional
source image for image-to-image. Sizes target what the default models are
tuned for, and sources are fitted to that size before upload, so large
photos cannot exhaust GPU memory.
- generate_video: prompt, aspect, duration up to 10 s, optional start frame.
- edit_image: crop, rotate, flip, resize to a size or format (crop, pad or
blurred background), extend, convert, in that order, with sharp; an optional
prompt repaints the result on ComfyUI. Edits keep the source as parent.
- Registrations declare the input artifacts so jobs refuse ones the user
cannot read, and generation declares a fixed GPU memory floor (6 GB images,
12 GB video) that a custom workflow cannot lower.
- Tool definitions reuse the operation input schemas plus continue_after.
- Saving media settings rejects custom workflows the provider could not fill.
- Tests: sharp edits on generated fixtures, handlers against the ComfyUI
emulator, and a PostgreSQL suite running the operations through the media
runtime (card outputs, artifact versions, foreign artifacts, GPU wait).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(media): share the ComfyUI JSON request helpers
Queue, interrupt and prompt requests now go through one JSON helper, so the
cancel path reads like the rest of the client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): register video operations with the media runtime
edit_video and export_video now run as media jobs: both registrations join
the shared operation list the API and the worker build their runtime from,
name every artifact they read (source, appended clips, subtitle files) so
starting a job refuses artifacts the user cannot read, and their tool
schemas use the runtime's continue_after helper.
Container deployments get ffmpeg, ffprobe and the subtitle font in the
API/worker image. Tests cover the handlers against a fake job context:
cuts, blurred 9:16 reframes with subtitles, appended silent clips, audio
extraction, single frames, batch exports with posters and progress,
rotated phone video, clear input errors and cancellation. They skip where
ffmpeg is not installed; the planner and parser tests always run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): describe media tools by what the model can rely on
The image server is the deployment's, not necessarily the user's, and
repainting an edit only works when generation is available.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): media tools, media RPCs and owner-only media settings API
Bots and users need to start and follow media jobs, and the deployment owner
needs to point the deployment at their GPU and transcription servers.
- Media tools are named like their operations. A run sees one only when the
space can run that operation (MediaRuntime.capabilities); the per-bot
allowlist still applies. applyTool hands every media tool to one
dispatcher (media-tools.ts) that splits off continue_after, starts the job
under the tool call's effect key (a replay returns the same job) and returns
at once; refusals (invalid input, unknown artifact, unavailable) go back to
the model as tool errors.
- media.jobs.start starts an export, transcription or image edit from a card:
the conversation is resolved like every thread procedure, the card's bot
(or the group's lead) posts the new card, and the runtime refuses source
artifacts outside the actor's space. media.jobs.get and media.jobs.cancel
only see the actor's own jobs.
- media.settings.get/update are deployment-owner only, never return keys and
answer invalid URLs with a 400 that says what is wrong.
- createApp accepts media operations as a test seam.
- Tests: a Postgres suite over the RPCs (owner-only settings, encryption,
key retention per server, URL policy, start/get/cancel isolation between
users, card jobs end to end) and unit tests for tool exposure and dispatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): transcribe audio and video into subtitles as a media job
With the media runtime in place, transcription becomes an operation like the
others: bots call the transcribe tool, the job runs in the background and
the card shows the subtitle files.
- createTranscriptionOperations registers transcribe: available when a
transcription server is configured, refuses artifacts the user cannot
read before the job exists, and writes <name>.srt and <name>.vtt next to
the source (a .txt when the server returns no timings). With ffmpeg the
audio is converted to 16 kHz mono WAV and long media is transcribed in
upload-sized ranges on one timeline; without it audio is sent as stored
and video is refused with a clear message. A server that is down or busy
(unreachable, 429, 502, 503) defers the job instead of failing it, since
nothing has been stored yet. The bot gets the transcript, cut at a word.
- The transcribe tool definition uses the shared media schema helper and
continue_after; dispatch stays generic.
- The main bot's run creates Studio once a media server is configured,
right before the specialist directory is built.
- Dictation: when the user's voice provider cannot transcribe (or there is
none), /api/voice/transcribe uses the self-hosted transcription server,
and the voice status reports server dictation so the web app uses it
instead of the browser's speech recognition. A voice provider that
transcribes keeps being used as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(notifications): keep the push sender contract next to the fan-out
The payload, target and outcome types are shared by the Expo and web push senders, so they now
live with PushSender in push-notifications.ts instead of in the web push adapter. The settings
E2E also stubs the Notification permission (headless Chromium reports it as denied even when
granted), and the quiet-hours inputs are wide enough for 12-hour times.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): play video and audio inline and stream uploads and downloads
Media sent in a conversation had nowhere to go: the composer capped every
file at 10 MiB and pushed it through a base64 RPC, video and audio blocks
did not render, and downloads decoded whole files in the page.
- Video and audio blocks play inline through MediaPlayer, which loads
nothing until the player is near the viewport, sizes the frame from the
block's dimensions (vertical clips stay within the column) and uses the
poster when one exists. Files the browser cannot play fall back to their
name with a download action.
- Images load straight from the artifact route; the lightbox gains a
download action.
- File cards, the Markdown preview and downloads read from the artifact
route instead of decoding base64.
- The composer accepts each kind up to its cap and names the computed
limit when it skips a file; video, audio and anything above the inline
cap upload as a raw stream, small files keep the inline RPC.
- MediaPlayer and artifactUrl are what the media card builds on.
Tests: MediaPlayer laziness, sizing and fallback, the upload helper, and an
E2E that uploads a tiny MP4 fixture (VP9, since Playwright's Chromium has
no H.264 decoder), plays it from the artifact route and captures
media-video-block, plus the image lightbox download.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(media): run transcribe through the media runtime on Postgres
Covers what the unit tests fake: the job starts from validated input with
the "Transcript" card, runs against the transcription emulator, stores the
.srt and .vtt as artifacts next to the source and lists them on the card,
and an artifact of another user is refused before a job exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): disclose media servers in AI data sharing
Media tools and card actions send prompts, images, video and audio to the
deployment owner's ComfyUI and transcription servers. Like the embeddings
server behind built-in memory, those self-hosted endpoints are recipients in
the AI data-sharing disclosure: they appear with the model uses ("ComfyUI
(<origin>)", "Transcription (<origin>)", keyed by the full endpoint so a new
server asks again), and starting a card job asks for model consent on clients
that run the foreground check.
Also adds unit tests for the media settings store: URL normalization and
refusals, encryption, the key-free view, one decryption per saved version, key
retention per server and removing empty settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(media): trim unused ffmpeg wrapper options
Binary paths, the ffmpeg version, the probe's container name and a
configurable stall timeout had no caller; detection is cached per process,
so every helper looks the binaries up itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): hand the transcript to the bot as quoted speech
A transcript is whatever was said in the recording, so the summary the bot
is woken with quotes it and says it is not instructions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): media settings section for the deployment owner (wip)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): give agents attached media and larger bot files
Agents only received the documents of a turn on their computer and could
attach workspace files up to 10 MiB, so they could neither cut a clip a user
sent nor post the video they rendered.
- Current-turn images, video and audio are copied into the bot's workspace
(Team Computers under bots/<botId>/) like documents. Remote computers move
files as base64 JSON, so anything above their 12 MiB transfer cap stays in
storage and the agent is told it by artifact id instead of failing the
turn; local computers copy up to the kind cap.
- attach_file accepts video and audio by extension and attaches up to the
kind cap when the store streams (still bounded by the transfer cap of a
remote computer), stored through the shared artifact helper with a real
SHA-256.
- The render_plot fallback that attaches a chart PNG now records the group,
so the chart opens in group conversations.
- Agent history names image artifacts by id, like video and audio, so a
later turn can refer to them.
- artifacts.get refuses files above 25 MiB: base64 reads hold the whole
file in memory, and larger media is served by the artifact route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(mobile): show video, audio and media cards as plain rows
Mobile has no media UI yet, so video, audio and media-card blocks rendered
as empty bubbles or fell out of previews.
- Thread text and reply previews describe video and audio files and the
media card's title and status.
- The bubble lists video, audio and a finished card's outputs as tappable
rows that open the file like other attachments.
- Opening video or audio downloads it from the artifact route to the cache
instead of a base64 RPC, so long recordings open without the server
holding them in memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): download media outputs with the forced-download artifact URL
The media card's download link uses artifactUrl's download option so a video
or audio output is saved rather than opened inline, and the E2E seeds the
thread snapshot with fresh response headers so the patched body is read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(api): refuse artifact uploads from untrusted origins
Uploads with a text/plain, text/csv or text/markdown body are CORS "simple"
requests that a browser sends cross-site without a preflight. The session
cookie's SameSite default already keeps it off such requests, but a
deployment that relaxes it would accept uploads from any page. The route now
applies the same origin rule as CORS before reading the session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(core): load child_process normally in the git checkout reader
readGitCheckout loaded node:child_process through require(), which does
not exist in the ESM API runtime. Linking a project space to a folder
that is a git checkout therefore failed after the space was created.
The lazy load is no longer needed: the reader is only reachable through
the server-only "@rakazo/core/git-checkout" subpath, never through the
package entry the web app bundles. Import spawn at the top instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): ask before discovering projects and let the user choose
The onboarding scan started on its own, discarded what it found, claimed
to read email and calendars, simulated progress for everyone who could
not scan, and its project creation always failed (wrong Space fields and
a space lookup by title across tenants).
Discovery now asks first and creates nothing without a choice:
- The scan step appears only for the deployment owner when
ONBOARDING_SCAN_HOME is set; everyone else skips it. The simulated
scan is gone.
- It shows the folder and asks. Only directory listings are read: git
projects up to four levels deep, their languages and a few tools
(Docker, GitHub Actions, Terraform). Symlinks are never followed and
each chosen folder is resolved again before use, so nothing outside
the folder is reached. Results stay with the account that scanned.
- Found projects are a checklist (none preselected); found languages and
tools are facts (preselected). "Add" creates one project space per
chosen project with createSpaceForMember and wireProjectWorkspace in
the user's own organization, reusing the user's own space already
linked to that folder, and saves the chosen facts as onboarding facts
under the profile lock and fact limit.
- When a connection becomes connected (direct connect, authorization
round trip, or catalog reconciliation), "Uses <App>" is remembered as
a tools fact from the connector. Revoking keeps it.
The E2E harness now points ONBOARDING_SCAN_HOME at a fixture folder and
starts without a deployment owner, so owner-only onboarding steps appear
only in specs that take the seat through /__e2e/deployment-owner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): start exports, transcriptions and cancels from the media card
The card's follow-ups now reach the media job RPCs, and it only offers what
the space can run.
- media.capabilities answers which operations the actor's space can run
(booleans only). The card asks once per minute per space, and only when it
shows finished video or audio, so other cards cost nothing.
- Export and Transcribe call media.jobs.start with the card's message, so the
same bot posts the new card; Cancel calls media.jobs.cancel. A refused
start shows the server's message under the card.
- New files use top-level import type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): GPU guard for smaller cards, top-level type imports
- The GPU guard waited for free VRAM at the operation's floor even on a card
whose total memory is below it (an 8 GB card and a 12 GB video floor), so
such jobs deferred until their budget ran out. The requirement is now capped
at 90% of the card's total; a card with less than half the floor fails at
once with "The GPU does not have enough memory for this job."
- Tests pin that media tools reach a run only through the space's
capabilities and within the bot's allowlist now that the image tool
definitions exist.
- Type-only imports are top-level `import type` in the media runtime, tools,
RPCs and tests (AGENTS.md).
- The ComfyUI settings test gives saveMediaSettings a store with nothing saved,
since the save reads the previous settings before validating.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(media): run video edits and exports through the media runtime
A Postgres suite starts edit_video and export_video through the real
runtime and local artifact store: outputs become linked versions with
posters on the card, exports store one version per preset, appended clips
and subtitle files of another user are refused at start, and an
out-of-range cut fails the job with a readable error. It skips without
Postgres or ffmpeg.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): abandoned handlers cannot crash the worker, sweep stale temp dirs
- A handler that ignores its abort is left behind after the grace period;
a late rejection from it (or from the grace timer after the job settled)
was unobserved, and the worker exits on unhandled rejections. Both sides of
the race are now observed and the grace timer is cleared with the job.
- Ending a job removes its temp directory when no runner in this process
holds it (leftovers of a runner that died here), and reconciliation sweeps
directories of jobs that no longer run.
- An executor test runs a media tool end to end through applyTool: shown
only for operations the space can run, started with the run's scope and
effect key, continue_after split off, refused when not exposed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): offer media card follow-ups on any settled job
Failed or cancelled jobs can still carry finished outputs, so the card asks
for the space's capabilities whenever a settled job shows video or audio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): coach on the Heute page
Heute only listed runs and linked to a hard-coded local Mission Control
address, so the coach's work had nowhere to show.
- Today's brief at the top, habits with a check-off and their streak,
and active goals, each only when there is something to show; then the
existing needs-you, doing and done lists.
- One icon button opens the check-in times (morning, evening, weekly),
loaded when opened and saved on every change.
- The page reads coach.today, refreshes it when a run finishes or the
tab becomes visible, and checks habits off optimistically; the date
header uses the user's time zone from the profile.
- Removes the Mission Control link and its constant.
- Tests: helper unit tests and a web E2E that writes the brief through
the main bot, checks a habit off across a reload and sets check-in
times (screenshots heute-coach and heute-rhythm).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(onboarding): walk the onboarding questions end to end; mobile reads cards as text
- onboarding-intake.spec walks focus, goal (after typing into the composer, which still
works), check-ins, quiet hours and the notifications card when the browser offers it,
with the onboarding-intake-goal, -rhythm and -done screenshots.
- completeOnboarding and the wizard specs drop the preferences step; the scan step is
optional (skipped when it asks, otherwise it advances by itself).
- The user-profile spec now checks the browser-filled profile and the focus fact; the
conversation spec checks the shorter app suggestion and the following goal question.
- The hover spec measures a reply that fills its row (a bot row's rail sits at the row's
end, so the old long onboarding line was what made it pass) and scopes Send to the
composer, since an open goal card has its own.
- Mobile shows a choice card as its question and answer instead of an empty bubble.
- The focus options drop their unused summary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): stub the Notification permission without a static-only class
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): let bots title image edits in the user's language
Edit cards showed English words such as "crop · flip · extend" to every user.
edit_image now takes an optional title written by the bot in the user's
language, used for the card and the file name like edit_video. Without one,
the card falls back to the repaint prompt or to language-neutral tokens only:
sizes, ratios, degrees, arrows and format names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(media): transcribe real clips through ffmpeg
Generated clips (nothing binary committed) go through extractAudio: a video's
audio reaches the server as 16 kHz mono WAV, a 12-second recording is
uploaded in two ranges under a small limit, and a video without sound is
refused before anything is uploaded. Skipped where ffmpeg is missing, which
may include CI runners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): cover group uploads in the main conversation
The image E2E now runs in the main conversation, which is a group thread,
so the browser test covers group uploads and reads from the artifact route
as well as bot threads. Bot file attachments keep their run's operation id
for tracing, and the non-streaming attach cap is the inline cap directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): title transcription cards with what they transcribe
A card that only says "Transcript" does not tell two transcriptions apart,
and it is English whatever the user's language.
- transcribe takes an optional title like the video operations, so a bot
names the card in the user's language; "Transcript" stays the default.
- Transcribe on a media card passes the source file name as the title.
- The E2E seeds the card through bootstrap as well as threads/get, since the
bot thread's first snapshot arrives with bootstrap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(web): match the media player stand-in to the pipeline's props
The pipeline workstream's MediaPlayer takes artifactId, kind, name, poster
and size, without a MIME type; the card and the stand-in now use exactly
that, so its file replaces the stand-in without other changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): media settings E2E with the media-settings screenshot
The Media settings section (added in the previous web commit) lets the
deployment owner point media at their own servers: ComfyUI URL and optional
key, with the image model and custom image/video workflows folded under
Advanced, and a transcription URL with optional model and key. Saved keys are
never shown again; the key field says one is saved and offers to remove it.
Members who do not own the deployment do not see the section.
The spec fills both servers, captures `media-settings`, saves and checks the
exact update the server receives, then removes the saved key and the
transcription server; a second spec checks a member has no Media section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): size uploaded videos from their own frame
Uploaded videos carry no dimensions, so a vertical clip sat letterboxed in
a 16:9 frame. The player now takes the frame size from the file's metadata
when the block has none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): disclose the transcription server before dictation uses it
Dictation falls back to the owner's transcription server when the voice
provider cannot transcribe, so that server is a voice recipient too: it is
listed for voice use (as "Transcription (<origin>)") whenever it would take
the recording, and not when the voice provider transcribes itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): render media card outputs with the URL-based previews
The media pipeline moved image and file previews to the artifact route, so
they no longer take a bot or group target. The media card still passed one,
which no longer type-checks; only its follow-up actions need the target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(api): let large uploads finish and abort stalled ones
Node's default 5-minute requestTimeout cut off a 500 MiB video upload on
any uplink slower than about 14 Mbit/s, so the video cap was unusable over
ordinary connections.
- The API server's requestTimeout is 30 minutes, enough for the largest
upload at about 2.5 Mbit/s. headersTimeout keeps its 60-second default.
- Each streamed upload aborts when the client sends no bytes for 60
seconds: the route answers 408, the partial file is deleted and no row
is written, so a slow-drip client cannot hold a connection for the full
request timeout. Time spent writing to disk does not count.
- The store and the upload guard no longer wait for a body to confirm its
cancellation, which a stalled or hostile client may never do.
- A guard test checks that every file the executor attaches records the
thread's group, which the render_plot chart fallback once dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): use snake_case keys in every media tool input
The model saw edit_video and export_video with artifact_id next to
generate_image, generate_video, edit_image and transcribe with artifactId,
sourceArtifactId and negativePrompt. Mixed conventions in one tool list invite
wrong calls, so all media inputs now use snake_case like continue_after and the
other builtin tools: artifact_id, source_artifact_id, negative_prompt,
duration_seconds. Handlers, artifact checks, the media card's transcribe
action and the tests follow. Nothing has shipped, so stored job inputs need no
migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): act on media cards in their own space, not the selected one
The main conversation lives in the home space, but the user can have another
space selected while it is on screen, and the card's actions sent that space:
media.jobs.start resolved the conversation in the selected space and failed,
and media.jobs.get/cancel only found jobs of the selected space.
The server now takes the space from the card itself. start finds the card's
message in one of the actor's own threads (the conversation named by botId or
groupId), requires the actor to still be a member of that thread's space, and
resolves the conversation and the source artifacts there. get and cancel find
the actor's job by id and require membership of the job's space.
media.capabilities takes an optional job id and answers for that job's space;
the card asks through its job and shares the answer per conversation. Every
other case is a 404, as before.
A Postgres test runs a card in the home-space main conversation while another
space is selected (start, get, capabilities, cancel) and checks that another
user and a user who left the card's space get 404 on every procedure.
This also completes the media settings section committed earlier as work in
progress (01eea83); its code was final, only that commit message said
otherwise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): find the sidebar Today button by its English label
The shell contrast test added on main looks up the sidebar button by the
old German source text, which this branch replaces with "Today".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* style: use top-level type imports in the media changes
AGENTS.md asks for top-level `import type` instead of inline `type`
specifiers; this applies it to the import statements the media work added
or changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore(web): extract catalogs and translate the media strings to German
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): show the onboarding cards and check-in names in the user's language
The onboarding cards, the closing line and the coach routine names were English server text
for every user. The server keeps the state and an English fallback; the web now words them
through Lingui, so there is one translation source.
- Choice cards: the web says the question, the option labels and the placeholder from the
step and option ids, and keeps the server text for anything it does not know (quiet-hour
ranges are already written in the user's locale). Another bot's focus card is recognized
by its option ids. Mobile and agent history keep the server text.
- The "Got it. … are a good place to start." line is gone: the app cards speak for
themselves, and with no cards nothing is said.
- The closing line becomes an intake_note block (note "done", nextCheckInAt, English text).
The web words it with the first check-in in the user's clock; mobile blockText and
blocksToAgentHistoryText read its text through their generic fallback.
- Routines carry coachKey. The routine list and editor show a check-in under its label in
the UI language until the user renames it; saving an untouched label keeps the stored
name. The English names move to contracts (COACH_ROUTINE_NAMES); prompts stay English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* style: use top-level type imports in the coach changes
AGENTS.md asks for top-level `import type` instead of inline `type`
specifiers; this applies it to the import statements the coach work added
or changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore(web): extract catalogs and translate the coach strings to German
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): look for the bot reply in the transcript only
The main conversation's sidebar row now previews the latest reply, so the
unscoped text lookup matched the sidebar too and failed in strict mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): drop the trading status from the shell contrast test
main's contrast test now waits for the sidebar trading status before
measuring; this branch removes that status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(onboarding): link the foreign folder while its tenant owns the deployment
Since #112 only the deployment owner can link a host folder, so the
discovery test creates the other tenant's linked space while that tenant
holds the deployment, then hands the deployment to the scanning owner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore(web): extract catalogs and translate the in-app browser strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): write poster frames as full-range JPEG
Newer ffmpeg releases refuse to encode JPEG from limited-range video
frames, so poster frames failed on CI's ffmpeg while older versions
accepted the same command. The poster is converted to full-range
yuvj420p before encoding, which every supported ffmpeg version takes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: look up the home main conversation in the scheduling journey
Since every project has its own main conversation, the journey that
schedules for a specialist found the user's main conversation with
findFirst over all of them. With the Lab project's conversation also
matching, it picked either one, so the check failed about one run in
three. The lookup now names the home space, where the specialist joined
and the routine is created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: look up the home main conversation in the scheduling journey
Since every project has its own main conversation, the journey that
schedules for a specialist found the user's main conversation with
findFirst over all of them. With the Lab project's conversation also
matching, it picked either one, so the check failed about one run in
three. The lookup now names the home space, where the specialist joined
and the routine is created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: expect the home main conversation's ask in another space's inbox
Every project now has a main conversation of its own, so an ask waiting in
the home main conversation is not visible from a project and its inbox
lists it, answered in the home space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(coach): move check-ins when the user's time zone changes
The coach check-ins are main-bot routines scheduled in the profile time zone, but a later
zone change (About you, onboarding, the main bot's update_user_profile tool, or forgetting
the whole profile) left them in the old zone until the rhythm was saved again, so they fired
at the wrong local time.
syncCoachTimeZone, next to the rhythm code, moves the main bot's coach routines that are not
in the current profile zone into it, recomputes the next run of armed ones and re-arms their
wakeup. Paused check-ins move but stay paused, routines the user made are never touched, users
without check-ins get none, and a save that leaves the zone as it is changes nothing. The
profile update and forget RPCs and the profile tool call it whenever they write the zone.
Quiet hours, the daily nudge cap and coach dates are evaluated in the profile zone when they
are used, so they need no migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): word the main conversation preview in the user's language during onboarding
While onboarding wrote the main conversation, its sidebar row, project rows and Heute showed the
server preview: nothing for a pending card (a card has no text of its own, so the row fell back
to "Ready") and the English fallback of the closing note, even for a German user.
Groups now carry `previewBlock` next to `preview`: the latest message when it is an onboarding
card or note, validated with the same block schemas the messages use (now named exports). The
web words it with the helpers that render the card and the note, so no copy is duplicated: the
question while a card waits, the answer once given, and the closing note in the user's language
and clock. A dismissed card keeps the plain preview, as the conversation no longer shows it.
The field is left out for every other message, and mobile keeps using the plain `preview`.
The chosen-answer lookup moves from the card into onboarding-copy so the card and the preview
share it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): stream large media between the store and local computers
Attachments copied into a local computer's workspace were read whole from
the artifact store first, so a 500 MiB video sat in memory before it was
written to disk. attach_file did the same in the other direction: a large
file on a local computer was read whole before it was stored.
SandboxProvider gains optional readFileStream/writeFileStream. The desktop
(host workspace) provider implements them with the same path confinement
as its byte methods, one chunk in memory at a time, and removes a file
whose write fails or is aborted, including one that replaced an earlier
copy. The host-aware wrapper routes them, and providers without them
(remote computers, still capped at 12 MiB) receive the collected bytes.
Current-turn staging and attach_file now go through these helpers, and
unread streams are cancelled so the stored file is released. Media job
inputs stop copying when the job is cancelled.
The stream helpers move from the local artifact store into a shared
module so both use one implementation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): live ComfyUI progress over its websocket
A video generation runs a single ComfyUI prompt for minutes, and the media
card only advanced after each prompt finished, so it sat still until the
clip was done.
The ComfyUI provider now follows ComfyUI's standard /ws stream while its
prompts run and maps step progress to the job's overall fraction across
all prompts: never backwards, and short of each prompt's share until its
outputs are downloaded. The history poll stays the source of truth for
completion and errors; a socket that cannot connect, drops, or stays
silent leaves generation exactly as before, and it is closed on finish,
failure, cancel and abort. Each generation uses its own client id, so
concurrent jobs do not take over each other's messages.
The socket goes through a new createOpenAiCompatibleWebSocket next to the
guarded fetch: same URL and key checks, same resolved-address lookup,
ws/wss derived from http/https, the same Authorization header, a message
size cap, and a hard drop when a server never finishes the closing
handshake. It uses the undici package's WebSocket, so no dependency was
added. A provider given a replaced fetch without a matching socket opens
none, so tests and emulators never reach the network.
The ComfyUI emulator now serves the websocket and reports progress for
running prompts; provider, guard and conformance tests cover ordering,
multi-prompt mapping, unavailable and failing sockets, stray messages,
closing on cancel, and the address checks against a loopback server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): look for the focus question in the conversation, not the sidebar
The main conversation's sidebar row now shows the pending intake question,
so a page-wide text match finds it twice (strict mode) or never reaches zero.
The specs mean the card in the open conversation, so they look there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(web): move Shell.tsx module-level code into pages/shell modules
Shell.tsx had grown to about 8,000 lines, roughly 2,200 of them module-level
components and helpers below ShellPage. That made the page hard to navigate
and review and turned every chat change into an edit of one hot file.
This is a move-only refactor: code is moved verbatim and only imports and
exports change. No renames, no JSX or memo boundary changes.
- shell/transcript.tsx: Transcript, QuoteSelectionButton, MessageHoverActions,
MessageView
- shell/composer.tsx: Composer, slashActionLabel, MentionOptionIcon,
MentionChipIcon, ATTACHMENT_ACCEPT
- shell/computer-screen.tsx: ComputerReleaseActions, embeddableScreenUrl,
screenIframeSandbox, DesktopKindEmptyState, computerPlaceholder,
computerLabel (all used by the computer pane)
- shell/thread-state.ts: threadSnapshotSignal, firstThreadRoute,
applyThreadEvent, newClientNonce
- shell/roster-format.ts: stripSpaceTitlePrefix, formatRosterTime,
cleanRosterPreview
- shell/message-preview.ts: previewMessageText (shared by composer and
transcript)
- shell/collapsed-sidebar-sections.ts: collapsed sidebar storage helpers
- shell/shell-types.ts: PendingAttachment and FALLBACK_BOT_COLOR, shared by
Shell.tsx, composer and transcript
Types, constants and one-off helpers used only by ShellPage stay in
Shell.tsx. The Lingui catalogs only change their file-reference comments;
message ids and translations are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(web): extract sidebar model and space search hooks from ShellPage
ShellPage mixes the sidebar's derived roster with thread orchestration in one
3,000-line body, which makes both hard to review. Behaviour-preserving move:
- useSpaceSearch owns the sidebar query, the debounced space search and
jumpToSearchHit.
- useSidebarModel owns the sidebar memos (roster groups, conversations,
projects, main conversation entry) and the project drag-and-drop order.
Both are called where the sidebar memos were, so the search effect keeps its
place among ShellPage's effects; bodies and dependency arrays move verbatim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(web): extract sound, Today count and browser notification hooks
Behaviour-preserving move of ShellPage side effects into named hooks:
- useShellSounds: audio ducking, the app-start sound and the sounds for
bots that need the user, finished work, run errors, handoffs and group
sparks.
- useHeuteActionCount: the 15-second Today brief poll behind the sidebar
badge.
- useBrowserNotifications: pending and delivered browser notification
bookkeeping plus notifyBrowserForEvent / flushPendingBrowserNotifications.
The hooks run where the code was. The Today poll effect sat between the
app-start sound and the bot status sound; it now runs just before the sound
hook, ahead of the two mount-only audio effects it does not interact with.
Effect bodies, refs and dependency arrays move verbatim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(web): extract mobile sidebar, bots sidebar and computer viewport hooks
Behaviour-preserving move of layout state out of ShellPage:
- useMobileSidebar: the mobile navigation open state, closing it at the
desktop breakpoint, and the edge-swipe touch handlers that were inline on
the shell root.
- useBotsSidebarCollapse: the per-user collapsed preference and the
edge-drag pointer handlers that were inline on the sidebar edge button.
- useComputerViewport: the on-screen-keyboard viewport tracking for the
full-screen computer.
Each hook is called where its effect was, so effect order is unchanged. The
inline JSX handlers become named functions with identical bodies, still
recreated on every render as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(web): extract messaging status, agent skills and mention target hooks
Behaviour-preserving move of ShellPage data loaders into named hooks:
- useMessagingStatus: the messaging surface flag and providers, loaded once
a session user exists.
- useAgentSkills: the agent skill catalog for slash commands, its refresh,
and the setter bot settings use after editing skills.
- useMentionTargets: composer mention options built from bots, group
members, routines and connectors, including the routine/connector loader.
Each hook is called where its effect was, so effect order is unchanged, and
the hooks take the same objects the effects read so bodies and dependency
arrays move verbatim. The mention loader's bots ref stays private to the
hook; nothing else read it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(web): extract ShellPage sidebar JSX into components
ShellPage's JSX carried the whole sidebar inline, including two near-copies
of the group header and of the main header's sidebar toggles, which made the
render hard to follow and easy to change in one place only.
Move-only: the sidebar header, group header (one component for the
conversation and project variants), the team and project roster rows, the
archived section, the user menu and the sidebar toggle buttons now live in
pages/shell. DOM, classNames, test ids, aria attributes, handlers and messages
are unchanged; the catalogs only update their file references, and the
desktop source checks for the header toggles now read the file they moved to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(web): extract ShellPage side panel and computer JSX into components
The side panel header, the computer preview and the full-window computer
overlay were large inline blocks in ShellPage's render.
Move-only: they are now SidePanelHeader, ComputerPanelPreview and
FullscreenComputerOverlay in pages/shell, each with explicit props. The
booting screen, the routine list and the create-bot modal stay where they
are. DOM, classNames, test ids, aria attributes, handlers and messages are
unchanged; the catalogs only update their file references.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(web): share one PointerMenu for the space and section menus
The space and section context menus repeated the same dropdown with an
invisible trigger anchored at the pointer; only their label and items differ.
Move-only: PointerMenu in pages/shell renders that dropdown and both menus
pass their label, close handler and items. DOM, attributes and handlers are
unchanged, and the labels stay in ShellPage, so the catalogs do not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
---------
Co-authored-by: Claude <noreply@anthropic.com>
* chore: remove legacy orchestrator, trading and one-off script leftovers
The repository still carried the pre-evOo prototype: a Python orchestrator,
the old HTML "evoo-platform" cockpit (whose test ran in the root unit
suite), an apex-trading package, the hot-reload deploy.sh for it, a
permissions manifest, an architecture note with local file links, agent
handoff notes, a Steve Jobs persona template and about ninety one-off
scripts that probe a developer database with hard-coded local URLs. None
of it is used by the product, CI or the real deploy in ops/evo-deploy.
Delete those files, drop the evoo-platform test glob from the root vitest
config, the Cockpit section from .env.example and the dead biome and
gitignore entries, and update the docs that listed the removed tools.
scripts/start-tmux.sh no longer starts the removed sentinel and resolves
the checkout from its own location instead of a machine-specific path;
scripts/seed-ollama.ts requires DATABASE_URL instead of falling back to a
local URL with a username.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): add media contracts, schema and provider interfaces
Groundwork for image and video generation, video editing and transcription.
Nothing calls it yet; the media runner, providers, tools and UI build on it.
- Contracts: video, audio and subtitle MIME types, per-kind size caps for
streamed and generated artifacts, video/audio message blocks and the live
media card block, media operations, job statuses and output presets
(9:16, 16:9, 1:1, 4:5, 4:3, 21:9).
- Core: attachment kinds include video and audio, artifacts become video or
audio blocks, and agent history keeps media artifact ids so later turns
can refer to them.
- Schema: artifacts record width, height, duration and the artifact they
were made from; media_jobs tracks generations, edits and transcriptions.
- Adapter kit: provider-neutral image generation, video generation and
transcription interfaces, the media job context operations run in, and
the MediaRuntime that tools use to start jobs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): drop trading and persona prompts from the conversation shell
The web shell still showed leftovers from the trading prototype: composer
chips with German trading and "Steve Jobs" prompts, trading suggestion
chips under the empty conversation line, a German trading hero for the
empty transcript, and a German "data protection" status panel in the
sidebar. They suggested actions the product does not have and added
persistent chrome.
Remove the composer chips and their helper, the empty-state chips and the
hero (the empty conversation keeps its existing one-line start hint), the
sidebar trading indicator and the data trust panel. The new-bot E2E now
asserts the plain empty state; the sidebar spec no longer expects the
trading status, and the placeholder Steve Jobs spec with no assertions is
gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor: remove the trading bot status integration
With the sidebar trading indicator gone, the only remaining consumer of
the trading bot status was a banner in General settings that reused the
same component. evOo is a personal assistant; a status proxy and poller
for one external trading service, configured through service-specific
TRADING_BOT_URL and VITE_TRADING_BOT_URL variables, does not belong in
the core product.
Remove the settings banner and its prop plumbing, the web poller, the
indicator component, the /api/trading-bot/status proxy, the shared core
contract, the env loading and both variables from .env.example, together
with their tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore: remove one-off database scripts and a stale review note
packages/db carried four hand-run scripts outside the package build that
query or seed a developer database with a hard-coded connection string,
one of them for a specific account. docs/VOICE-FIRST-REVIEW.md was a
one-time verification note for a workflow that no longer exists and
named private machines and local paths. Nothing references them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(memory): remember appends dated notes instead of replacing memory
The remember tool rewrote the bot's whole MEMORY.md with each call, so every
new fact erased the earlier ones unless the model re-sent the full document.
- MemoryCommitRequest gains mode "append" (the store joins the note onto the
current content inside the same serializable transaction and still writes a
revision row) and every commit is capped at 16 KB per document.
- remember appends a dated bullet ("- YYYY-MM-DD: ...", the user's local date)
by default and rewrites only with mode "replace"; a full document returns an
error that tells the bot to condense it. Retries of the same tool call stay
single appends through the existing effect record.
- Scripted runtime memory entries can append too; the scripted "remember"
script now appends instead of overwriting the bot's memory.
- When memory does not fit the run's 32 KB budget, the prompt keeps a
document's newest lines instead of its oldest.
- memory.update maps an oversized save to BAD_REQUEST.
- docs/PRODUCT.md records that UserFact is the cross-space user model and the
space MEMORY.md stays space notes (no migration).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(coach): goals, habits, check-ins, daily briefs and check-in rhythm
The main agent should coach the user, but nothing stored what the user
wants to achieve or keep doing, and no routine ever checked in. This adds
the data model and the interfaces the onboarding intake and the coach
tools build on.
- Schema: UserGoal, Habit, CheckIn (one per habit, goal or day and date)
and DailyBrief, all keyed to the user's local calendar date, plus a
coachKey on Routine that marks the main bot's coach check-ins.
- Contracts (coach.ts): CoachRhythm, Goal, Habit, CheckIn, DailyBrief and
CoachToday DTOs and the RPC inputs.
- Repository (@rakazo/db coach.ts): goal, habit and check-in CRUD with
per-user limits, upsertDailyBrief and loadCoachToday with streaks in
the user's time zone.
- coach-rhythm.ts: setCoachRhythm/getCoachRhythm keep up to three main
bot routines (morning brief, evening check-in, weekly review) in the
main conversation in step with the user's chosen times.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(notifications): add push subscriptions, notification settings and web push interfaces
Push could only reach one mobile device per user (a token file), quiet hours did not exist,
and the web app had no service worker, so a closed tab never heard about finished work.
This lands the shared interfaces first so onboarding can build on them:
- push_subscriptions (web push endpoints and Expo tokens, one row per device) and
user_notification_settings (quiet hours as minutes in the profile time zone, nudges, a
per-day nudge counter), with CHECK constraints.
- NotificationSettingsSchema and web push contracts, notifications.settings.get/update and
notifications.webPush.config/subscribe/unsubscribe (owner-scoped, https endpoints only).
- getNotificationSettings/updateNotificationSettings plus the policy and nudge-cap helpers.
- VAPID keys from VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY or generated once and stored encrypted,
and an SSRF-guarded web push sender built on the web-push package.
- The web app links its manifest, registers a push-only service worker where supported (secure
context, not the desktop app) and exposes webPushSupported/webPushEnabled/enableWebPush/
disableWebPush.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore: replace machine-specific paths and addresses with placeholders
ORIGIN.md pinned the intended checkout to an absolute path under a
personal home directory, docs/LOCAL-SETUP.md listed folders on one
developer's machine, and two tests used what looks like a real tailnet
address. Use a home-relative path, drop the machine-specific folder list,
and use 100.100.100.100 in the tests; it is still inside 100.64.0.0/10,
so both tests check the same thing as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(coach): repository, streak and rhythm suites
Covers what the coach interfaces promise before anything builds on them:
goal and habit ownership and limits, one check-in per habit, goal and day
dated in the user's time zone, streaks across weekends and weeks, and a
rhythm that keeps exactly one main-bot routine per check-in and is
idempotent when set again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(contracts): share one clock-time schema
The coach rhythm and the quiet hours both parse "HH:MM" wall-clock times
and each defined its own ClockTimeSchema, which the contracts index cannot
export twice. Both now import it from one module.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): ask the onboarding questions in the main conversation
The wizard asked for the account name again and a "Tell us about yourself" form, and the
focus answer in the main conversation was never saved. The main bot now gets to know the
user in the main conversation instead, one card after each answer.
- UserOnboarding keeps the pending step, the focus and when onboarding finished.
- onboarding-intake.ts declares the steps (focus, goal, rhythm, quiet hours, notifications)
as cards plus what their answers change; onboarding.ts commits an answer, the focus app
suggestions and the next card or the closing line in one transaction.
- The choice block gains a step, an optional text input and the typed answer; choose takes
the step, the text and whether the device can still turn push on.
- The focus answer becomes a goals fact; the goal, rhythm and quiet-hours effects and the
push seam are placeholders until the coach and notification modules are merged.
- The wizard drops the preferences step and fills time zone, locale and language from the
browser; the main bot's prompt notes open onboarding cards; agent history shows a card's
question and answer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): transcribe speech on any OpenAI-compatible server and write subtitles
Transcription has to run on the user's own hardware, and the only speech-to-text
path so far was the OpenAI voice key. Self-hosted whisper servers (whisper.cpp,
faster-whisper based servers, vLLM) all speak the OpenAI transcriptions
protocol, so one provider covers them.
- createOpenAiCompatibleTranscriptionProvider posts a multipart upload to
{base}/audio/transcriptions asking for timed segments, falls back to plain
JSON for servers that reject verbose_json, reads text/plain bodies, and
reports failures in words. It reuses the address-checked fetch and URL
guards of the other OpenAI-compatible adapters, caps uploads at 25 MiB and
responses at 8 MiB, and builds the multipart body by hand because the
undici fetch behind the address checks does not serialize the global
FormData class.
- An in-process server emulator (OpenAI/whisper.cpp, plain-JSON-only and
text-only flavours) and an in-process provider back an offline
conformance suite, plus one loopback test through the real fetch.
- toSrt/toVtt in core turn segments into SubRip and WebVTT, wrapping long
segments into cues of at most two 42-character lines that share the
segment's time, and escaping WebVTT cue text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(notifications): fan push out to every device, honour quiet hours, add notify_user
Run notifications reached at most one phone and nothing could hold them back at night.
- PushNotificationProvider sends each notification to all of the user's registered devices
(web push and Expo), skips everything during quiet hours in the profile time zone, drops
devices the push service reports gone (404/410, DeviceNotRegistered), records lastUsedAt and
links each push to its conversation.
- Expo tokens move from DATA_DIR/push-tokens files into push_subscriptions: the API imports and
deletes the legacy files at start; notifications.registerPush/unregisterPush keep their
contract for the mobile app (tokens must look like Expo tokens).
- NotificationMessage gains the nudge kind, and the main bot gets notify_user: a short push to
the main conversation that respects the nudge switch, quiet hours and a cap of three per day.
It is withheld from shared messaging channels and from runs answering another owner's agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(coach): main-bot coach tools and goals in its prompt
The main agent had no way to record what the user wants to achieve or how
their day went, and its prompt never showed their goals or habits.
- update_goal, update_habit, log_check_in and write_daily_brief, main-bot
only and, like update_user_profile, withheld from shared channels and
from runs that answer another owner's agent. Goals and habits can be
named by id or exact title.
- The main bot's own runs list active goals and habits (with ids, done
state and streak) in the <user> block; the basics-only block and every
other bot never see them. The main agent role adds a short coaching
stance and names each coach tool only when the run has it, so the
check-in routine prompts stay tool-agnostic.
- The scripted runtime understands a few coach commands and the morning
check-in, so tests can drive the tools end to end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): add the Studio specialist the main bot hands media work to
Media work (cutting for a format, subtitles, exports) needs editing judgement
and a focused tool set, so the main bot should be able to delegate it to a
visible specialist instead of doing it itself.
- ensureStudioBot creates "Studio" once per user, next to the main bot so it
can join the main conversation and read its media, tagged media, with
editor instructions (hook first, tight pacing, 9:16 safe areas, subtitles
for muted playback) and a restricted builtin allowlist: every media
operation plus files, questions, memory and web lookup. It has a thread,
so the main bot's specialist directory lists it. The spawn key carries the
user id so members of a shared space each get their own.
- A Studio the user archived or deleted stays gone. Deleted bots now record
their spawn key in bot_deletions so idempotent creation can tell.
- Postgres suite for concurrency, shared spaces, archive and delete.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): use English Lingui sources for German UI text
Several web surfaces hard-coded German (the project card, roster and
activity status labels, the sidebar "Heute" label) or used German text as
the Lingui source message (sidebar sections, project timeline, bot picker,
voice entry, group stage, copy and read-aloud buttons). English users saw
German, and because production builds keep only message ids, none of these
strings could be translated per locale.
Convert them to English source messages, keeping the meaning:
- ProjectCreatedCard: Lingui Trans/Plural instead of literals.
- agent-activity: labels go through i18n._ with literal ids so the module
stays importable from plain unit tests; the extractor still collects them.
- Shell, ProjectSessionTimeline, bot-picker, StageVoiceEntry, OoStage:
English sources, reusing existing messages where one already fit
("just now", "{n}m ago", "Search", "No bots match", "Set up voice",
"Group conversation", "Transcript").
- The conversation picker matches its create entry against the translated
label instead of a fixed German string.
The German wording returns through the de catalog in the next commit.
Unit and E2E tests now assert the English labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): keep Lingui catalogs complete and in sync
The committed catalogs were last extracted weeks ago: about 330 source
messages were missing from every locale, and production builds keep only
message ids, so those strings rendered as hash ids (sidebar sections,
privacy settings, profile, timeline, ...). German also had 226 empty
entries, and every extraction rewrote the files because references carried
line numbers.
- lingui.config.ts: po references without line numbers, and a stable sort
that breaks Lingui's ordering ties (explicit ids, equal text) by id, so
the output no longer depends on file-system order.
- Re-extract all catalogs with --clean (drops 25-44 obsolete entries per
locale; no existing translation changed).
- Translate every German entry (du form, consistent with the existing
catalog). Other locales keep empty entries and fall back to English.
- i18n-catalog-sync.test.ts extracts into a copy of the catalogs and fails
when the committed files differ or a German entry is empty, so stale
catalogs fail the unit test job (offline, about 5 s).
Adds @lingui/format-po (already installed through @lingui/cli) as a direct
dev dependency to configure the formatter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): add the ComfyUI provider, default workflows and emulator
Image and video generation run on the user's own GPU server. ComfyUI is the
common self-hosted backend, so this adds a provider for its HTTP API that the
generation operations can call.
- Core: API-format workflow templates with a small placeholder format
({{prompt}}, {{width}}, {{seed}}, ...) that is validated and filled without
evaluating anything. Defaults: text-to-image and image-to-image on core nodes
for any SD-family checkpoint, and text/image-to-video on ComfyUI's core
LTX-Video nodes; a README lists the placeholders and model files.
- Adapters: createComfyUiProvider submits a workflow, polls its history,
uploads input images and downloads outputs through the address-checked fetch
used for other self-hosted model servers, with timeouts, capped reads and
readable errors. A custom workflow from settings replaces the default it
matches. Cancelling dequeues or interrupts only its own prompt. GPU status
counts memory ComfyUI can free for itself as available.
- Contracts: input schemas for generate_image, generate_video and edit_image.
- Tests: an in-process ComfyUI emulator, a provider-neutral generation
conformance suite and provider tests, all offline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(memory): derive the run's memory budget from the document limit
The prompt budget was a separate 32 KB constant; it is now twice the
per-document limit, so a full bot document and a full space document keep
fitting if either number changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(profile): edit facts in place in About you
About you could only confirm or delete what the bots know about the user, so a
fact that was slightly wrong or filed under the wrong category had to be
deleted and taught again, and a fact could not be limited to some spaces from
the app even though the profile supports it.
- Each fact gets an Edit button that turns its row into an inline editor:
wording, category, and (when the user has more than one space, or the fact
is already limited) which spaces it shows in. Escape or Cancel leaves it
unchanged; only changed fields are sent through profile.update editFacts.
- A limited fact names its spaces next to its source.
- The repository validates edits like new facts: a wording whose normalized
form is empty or equals another fact of the user is refused, all edits of
one update apply atomically, and the stored embedding is only dropped when
the wording really changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(inbox): list and answer items from all of the user's spaces
The inbox in the main conversation only showed asks, approvals, takeovers and
unread threads of the current space. The main conversation lives in the home
space, so anything a bot needed in a project space never reached it.
- inbox.list now covers every space the user belongs to (except spaces being
deleted), still only the user's own bots, runs and threads. Items from
another space carry that space's id and name.
- The web inbox shows the space name next to the bot for those items, answers
them with that space's header so the existing thread procedures check
membership and ownership for that space, and opens their thread in it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(coach): coach RPCs for goals, habits, check-ins, today and rhythm
The web app needs to show and change the user's coach data, and the
check-in times need a way to change after onboarding.
- coach.today, coach.goals.*, coach.habits.*, coach.checkIns.* and
coach.rhythm.get/set, owner-only and not space-scoped; another user's
ids read as not found, invalid input as a bad request.
- coach-rhythm reuses the shared clock-time helpers.
- Tests: an API suite covering ownership and validation for every
procedure, the coach tools driven through the main bot (and refused
for a specialist), and a morning check-in routine that fires in the
main conversation and writes today's brief; the unauthenticated
authorization sweep lists the new procedures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): save goal, check-in rhythm, quiet hours and push from the cards
With the coach and notification modules merged, the onboarding answers now take effect:
- the goal card creates a goal (source onboarding) through createGoal;
- the rhythm card sets the main bot's check-in routines through setCoachRhythm, and the
closing line names the first check-in from the routines that were saved;
- the quiet-hours card updates the notification settings;
- the choice card tells the server whether this browser can still turn push on, so the
notifications card is offered only there, and "Turn on" subscribes before answering.
The thread lock becomes NO KEY UPDATE so a rhythm routine, which references the thread, can
be written while the answer holds the lock. The goal answer is capped at the coach title
length. A Postgres suite walks the questions end to end: answers, skips, a dismissed card, a
stale and a doubled answer, another bot's focus card, and another user's access.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): stream artifacts over HTTP with Range support
Videos and audio can be hundreds of megabytes. Artifacts only travelled as
base64 inside RPC calls, capped at 10 MiB and held whole in memory, so media
could neither be uploaded, played with seeking nor produced by media jobs.
- ArtifactStore gains stat, createReadStream (inclusive byte ranges),
putStream (size cap enforced while writing, real SHA-256) and an optional
localPath so ffmpeg can read stored files in place. LocalArtifactStore
implements them with the same safety as before (O_EXCL, O_NOFOLLOW, 0600)
and now validates space ids and keys before building paths.
- storeArtifactStream / storeArtifactFromFile store a stream or a job's
output file as an artifact row, with width, height, duration and parent,
and remove the blob again if the row cannot be written.
- GET /api/artifacts/:id serves an artifact with Range/206/416, ETag,
inline disposition for media and attachment otherwise (safe RFC 6266
filenames), nosniff and same-origin CORP. Authorization matches
artifacts.get: the owner's artifact of an active bot thread or group,
with membership of the artifact's space (media elements cannot send the
space header; an explicit header must name the same space).
- POST /api/artifacts takes a raw streamed body with the target and name in
the query, authorized like artifacts.create, with the MIME allowlist and
the per-kind caps checked from Content-Length and again while writing.
- The API CSP allows media from the app origin and blob URLs.
- Web: artifactUrl(artifactId) and uploadArtifactFile for the UI.
Tests cover the streaming store, range parsing, filename sanitizing, the
file helper, and a Postgres suite for both routes: ranges, caching, other
users, shared-space members, other spaces, archived threads, signed-out
requests and upload limits including chunked bodies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): media job runtime, media queue and media settings
Media operations (generation, edits, transcription) need a runner that works
outside the agent turn, keeps a card in the conversation current and never
lets two processes put work on one GPU. Operations land in later commits; this
is the runtime they register with.
- Background job `media.run { jobId }` on a named `media` queue. Jobs carry an
optional queue: the Graphile publisher passes it as queueName and the
in-memory driver runs each queue one job at a time, in order.
- `createMediaRuntime` implements MediaRuntime (capabilities, start, get,
cancel) over a list of `MediaOperationRegistration`s and runs jobs:
start validates the input, creates the job and posts the bot's media card in
one transaction (idempotent per tool call through an operation key), then
enqueues it. The runner claims a fenced lease (an advisory lock keeps one
live lease at a time, so the GPU stays single even when the queue is
bypassed), renews it while polling for cancellation, writes progress and
outputs into the card, defers with backoff when the GPU is short of memory
or the operation asks to wait, bounds deferrals, crash restarts and run
time, always cleans the job's temp directory and always ends the card in a
terminal state. With continue_after the starting bot is woken in the same
transaction that ends the job (new run trigger `media`).
- The reconciler re-enqueues jobs whose enqueue was lost, whose runner died
or whose wait is over; a job stuck behind a dead worker's queue lock runs
outside the queue.
- Media backends are deployment-wide settings stored encrypted as the
`media` IntegrationProviderConfig row: `loadMediaSettings` and
`saveMediaSettings` with the same address policy as OpenAI-compatible
servers (no metadata hosts, public hosts only when allowed, HTTPS when a key
is sent), a saved key never follows the settings to another server, and the
view never returns keys. Contract in contracts/media-settings.ts.
- `createMediaOperations` is the one list both the API and the worker build
their runtime from (empty until the operations land), and media-tools.ts has
the shared `continue_after` schema helper for the tool definitions.
- Migration: lease, deferral, operation key and continueAfter columns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(notifications): cover quiet hours, delivery, nudges and the notification RPCs
The quiet-hours CHECK accepted a row with only one bound set, because a NULL comparison made
the whole condition NULL; it now requires both bounds explicitly. The migration has not shipped
yet, so it is corrected in place.
- Unit: quiet-hours math across midnight, time zones and daylight saving; VAPID key handling
(configured, generated once and shared, invalid); the web push sender (encrypted VAPID-signed
request, gone on 404/410, no private or non-https endpoints); the fan-out with fake senders.
- PostgreSQL: settings and subscription repositories (constraints, per-user removal, endpoint
hand-over, device cap, concurrent nudge cap, cascade on account deletion), delivery and
notify_user against a real main conversation, and every notification RPC through the API
including cross-user isolation and endpoint validation.
- .env.example documents the optional VAPID keys and VAPID_SUBJECT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): notification settings, and sign-out stops this browser's push
Users had no way to turn on push for a browser, set quiet hours or opt out of nudges.
Settings gains a compact Notifications section: a switch for push on this device (only where
web push works: a secure context outside the desktop app), quiet hours with from/to times
(22:00-07:00 when first turned on) and a switch for nudges. Changes save in order as they are
made. A browser that blocked notifications says so instead of offering a dead switch. Logging
out unregisters the browser first, so a shared computer stops receiving the account's pushes.
The E2E stubs the Push API (headless Chromium has no push service), walks all three controls,
checks they survive a reload and captures the notification-settings screenshot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): transcription input, chunked transcripts and the Studio trigger
Groundwork for the transcribe operation and for creating Studio when it is
first needed.
- Contracts: TranscribeInputSchema (artifact id, optional language code).
- transcribeChunks uploads audio chunks in order and stitches them into one
transcript on the original timeline; transcriptionChunkSeconds sizes
16 kHz mono WAV chunks below the server's upload limit; transcriptSummary
hands the bot the transcript, cut at a word.
- ensureStudioWhenMediaConfigured creates Studio once a media server
(generation or transcription) is configured. Local edits alone do not
count, because ffmpeg and image tools are present on most installs and a
specialist nobody asked for would appear for everyone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): show media jobs as a live card in the conversation
Media jobs report progress and results through the media block, which the
web app did not render.
- MediaCard renders the block: operation icon, title, status (reusing the
task card's labels, plus "Waiting for GPU" for jobs queued behind the
GPU), a progress bar while active, the error when failed, and outputs:
images as thumbnails with the existing lightbox, video and audio in the
media player, subtitle and text files as file cards. Finished video and
audio get a download link, "Export" with presets labelled by aspect ratio
(the video's own shape left out) and "Transcribe", offered only when the
space can run them; running jobs get "Cancel".
- MessageView renders it with one branch.
- An E2E seeds the card on a thread snapshot with real uploaded artifacts.
- artifact-url.ts and MediaPlayer.tsx are minimal stand-ins for the
pipeline workstream's files and give way to them on merge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): edit and export video with ffmpeg
Bots can now cut, reframe and export videos for every common format without
a hosted service: ffmpeg and ffprobe from PATH do the work on the server.
- ffmpeg wrapper: PATH detection, ffprobe JSON probing (rotation, pixel
aspect, frame rate, audio, packet scan when the container has no
duration), runs with -progress parsing, abort and stall kills, an output
size cap, partial-output cleanup and short errors without temp paths.
Inputs go through the file protocol with the demuxer forced from the MIME
type, so a crafted upload cannot be probed as a playlist that reads other
files or URLs. extractAudio (16 kHz mono WAV) and renderPoster for
transcription and media cards.
- A pure planner turns edit steps (trim, keep, concat, reframe, speed,
volume, mute, subtitles, extract_audio, poster) into one filter_complex
pass: leading cuts seek the input, appended clips are normalized to the
frame, fps and 48 kHz stereo (silence where a clip has none), reframes
crop, pad or blur to the presets, speed chains atempo, subtitles burn in
from generated file names with a readable style that stays clear of the
controls on 9:16 frames. H.264/AAC MP4 with yuv420p and faststart;
location metadata is stripped.
- edit_video and export_video operations and tool definitions; the inputs
are shared contracts so the media card can start exports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): stream job inputs and outputs, cover the job lifecycle on Postgres
Media files can be hundreds of megabytes, so the runner no longer buffers them:
inputs are cloned from the local artifact file when the store has one and
streamed otherwise, outputs go through storeArtifactFromFile (per-kind size cap
and MIME check while streaming; an oversized result fails the job with a short
card error).
A Postgres suite runs the runtime with fake operations: success with outputs,
posters and parent versions; input, availability, artifact and thread checks
before a job exists; tool-call replay; failure with temp-dir cleanup; outputs
outside the temp dir and sources of another user; deferral with backoff and its
budget; the GPU memory guard; cancel of a queued job and of a job running in
another process; one job per lease and one job on the GPU at a time; crash
recovery and its restart limit; reconciliation inside and outside the queue;
the continue_after wake; a cleared conversation; the run time limit; and
capabilities.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(profile): keep the fact editor's Save label visible
Small buttons in ui-web merge their text-ui-label size over the default
variant's text color, so the small Save button rendered ink on ink. The
editor now uses default-size controls, with the category select at the same
height.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): generate images and video on ComfyUI and edit images
Bots can now make images and short clips on the user's own GPU server and
rework images locally, through the media job runtime.
- generate_image: prompt, aspect preset or exact size, up to 4 images, optional
source image for image-to-image. Sizes target what the default models are
tuned for, and sources are fitted to that size before upload, so large
photos cannot exhaust GPU memory.
- generate_video: prompt, aspect, duration up to 10 s, optional start frame.
- edit_image: crop, rotate, flip, resize to a size or format (crop, pad or
blurred background), extend, convert, in that order, with sharp; an optional
prompt repaints the result on ComfyUI. Edits keep the source as parent.
- Registrations declare the input artifacts so jobs refuse ones the user
cannot read, and generation declares a fixed GPU memory floor (6 GB images,
12 GB video) that a custom workflow cannot lower.
- Tool definitions reuse the operation input schemas plus continue_after.
- Saving media settings rejects custom workflows the provider could not fill.
- Tests: sharp edits on generated fixtures, handlers against the ComfyUI
emulator, and a PostgreSQL suite running the operations through the media
runtime (card outputs, artifact versions, foreign artifacts, GPU wait).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(media): share the ComfyUI JSON request helpers
Queue, interrupt and prompt requests now go through one JSON helper, so the
cancel path reads like the rest of the client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): register video operations with the media runtime
edit_video and export_video now run as media jobs: both registrations join
the shared operation list the API and the worker build their runtime from,
name every artifact they read (source, appended clips, subtitle files) so
starting a job refuses artifacts the user cannot read, and their tool
schemas use the runtime's continue_after helper.
Container deployments get ffmpeg, ffprobe and the subtitle font in the
API/worker image. Tests cover the handlers against a fake job context:
cuts, blurred 9:16 reframes with subtitles, appended silent clips, audio
extraction, single frames, batch exports with posters and progress,
rotated phone video, clear input errors and cancellation. They skip where
ffmpeg is not installed; the planner and parser tests always run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): describe media tools by what the model can rely on
The image server is the deployment's, not necessarily the user's, and
repainting an edit only works when generation is available.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): media tools, media RPCs and owner-only media settings API
Bots and users need to start and follow media jobs, and the deployment owner
needs to point the deployment at their GPU and transcription servers.
- Media tools are named like their operations. A run sees one only when the
space can run that operation (MediaRuntime.capabilities); the per-bot
allowlist still applies. applyTool hands every media tool to one
dispatcher (media-tools.ts) that splits off continue_after, starts the job
under the tool call's effect key (a replay returns the same job) and returns
at once; refusals (invalid input, unknown artifact, unavailable) go back to
the model as tool errors.
- media.jobs.start starts an export, transcription or image edit from a card:
the conversation is resolved like every thread procedure, the card's bot
(or the group's lead) posts the new card, and the runtime refuses source
artifacts outside the actor's space. media.jobs.get and media.jobs.cancel
only see the actor's own jobs.
- media.settings.get/update are deployment-owner only, never return keys and
answer invalid URLs with a 400 that says what is wrong.
- createApp accepts media operations as a test seam.
- Tests: a Postgres suite over the RPCs (owner-only settings, encryption,
key retention per server, URL policy, start/get/cancel isolation between
users, card jobs end to end) and unit tests for tool exposure and dispatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): transcribe audio and video into subtitles as a media job
With the media runtime in place, transcription becomes an operation like the
others: bots call the transcribe tool, the job runs in the background and
the card shows the subtitle files.
- createTranscriptionOperations registers transcribe: available when a
transcription server is configured, refuses artifacts the user cannot
read before the job exists, and writes <name>.srt and <name>.vtt next to
the source (a .txt when the server returns no timings). With ffmpeg the
audio is converted to 16 kHz mono WAV and long media is transcribed in
upload-sized ranges on one timeline; without it audio is sent as stored
and video is refused with a clear message. A server that is down or busy
(unreachable, 429, 502, 503) defers the job instead of failing it, since
nothing has been stored yet. The bot gets the transcript, cut at a word.
- The transcribe tool definition uses the shared media schema helper and
continue_after; dispatch stays generic.
- The main bot's run creates Studio once a media server is configured,
right before the specialist directory is built.
- Dictation: when the user's voice provider cannot transcribe (or there is
none), /api/voice/transcribe uses the self-hosted transcription server,
and the voice status reports server dictation so the web app uses it
instead of the browser's speech recognition. A voice provider that
transcribes keeps being used as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(notifications): keep the push sender contract next to the fan-out
The payload, target and outcome types are shared by the Expo and web push senders, so they now
live with PushSender in push-notifications.ts instead of in the web push adapter. The settings
E2E also stubs the Notification permission (headless Chromium reports it as denied even when
granted), and the quiet-hours inputs are wide enough for 12-hour times.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): play video and audio inline and stream uploads and downloads
Media sent in a conversation had nowhere to go: the composer capped every
file at 10 MiB and pushed it through a base64 RPC, video and audio blocks
did not render, and downloads decoded whole files in the page.
- Video and audio blocks play inline through MediaPlayer, which loads
nothing until the player is near the viewport, sizes the frame from the
block's dimensions (vertical clips stay within the column) and uses the
poster when one exists. Files the browser cannot play fall back to their
name with a download action.
- Images load straight from the artifact route; the lightbox gains a
download action.
- File cards, the Markdown preview and downloads read from the artifact
route instead of decoding base64.
- The composer accepts each kind up to its cap and names the computed
limit when it skips a file; video, audio and anything above the inline
cap upload as a raw stream, small files keep the inline RPC.
- MediaPlayer and artifactUrl are what the media card builds on.
Tests: MediaPlayer laziness, sizing and fallback, the upload helper, and an
E2E that uploads a tiny MP4 fixture (VP9, since Playwright's Chromium has
no H.264 decoder), plays it from the artifact route and captures
media-video-block, plus the image lightbox download.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(media): run transcribe through the media runtime on Postgres
Covers what the unit tests fake: the job starts from validated input with
the "Transcript" card, runs against the transcription emulator, stores the
.srt and .vtt as artifacts next to the source and lists them on the card,
and an artifact of another user is refused before a job exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): disclose media servers in AI data sharing
Media tools and card actions send prompts, images, video and audio to the
deployment owner's ComfyUI and transcription servers. Like the embeddings
server behind built-in memory, those self-hosted endpoints are recipients in
the AI data-sharing disclosure: they appear with the model uses ("ComfyUI
(<origin>)", "Transcription (<origin>)", keyed by the full endpoint so a new
server asks again), and starting a card job asks for model consent on clients
that run the foreground check.
Also adds unit tests for the media settings store: URL normalization and
refusals, encryption, the key-free view, one decryption per saved version, key
retention per server and removing empty settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(media): trim unused ffmpeg wrapper options
Binary paths, the ffmpeg version, the probe's container name and a
configurable stall timeout had no caller; detection is cached per process,
so every helper looks the binaries up itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): hand the transcript to the bot as quoted speech
A transcript is whatever was said in the recording, so the summary the bot
is woken with quotes it and says it is not instructions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): media settings section for the deployment owner (wip)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): give agents attached media and larger bot files
Agents only received the documents of a turn on their computer and could
attach workspace files up to 10 MiB, so they could neither cut a clip a user
sent nor post the video they rendered.
- Current-turn images, video and audio are copied into the bot's workspace
(Team Computers under bots/<botId>/) like documents. Remote computers move
files as base64 JSON, so anything above their 12 MiB transfer cap stays in
storage and the agent is told it by artifact id instead of failing the
turn; local computers copy up to the kind cap.
- attach_file accepts video and audio by extension and attaches up to the
kind cap when the store streams (still bounded by the transfer cap of a
remote computer), stored through the shared artifact helper with a real
SHA-256.
- The render_plot fallback that attaches a chart PNG now records the group,
so the chart opens in group conversations.
- Agent history names image artifacts by id, like video and audio, so a
later turn can refer to them.
- artifacts.get refuses files above 25 MiB: base64 reads hold the whole
file in memory, and larger media is served by the artifact route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(mobile): show video, audio and media cards as plain rows
Mobile has no media UI yet, so video, audio and media-card blocks rendered
as empty bubbles or fell out of previews.
- Thread text and reply previews describe video and audio files and the
media card's title and status.
- The bubble lists video, audio and a finished card's outputs as tappable
rows that open the file like other attachments.
- Opening video or audio downloads it from the artifact route to the cache
instead of a base64 RPC, so long recordings open without the server
holding them in memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): download media outputs with the forced-download artifact URL
The media card's download link uses artifactUrl's download option so a video
or audio output is saved rather than opened inline, and the E2E seeds the
thread snapshot with fresh response headers so the patched body is read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(api): refuse artifact uploads from untrusted origins
Uploads with a text/plain, text/csv or text/markdown body are CORS "simple"
requests that a browser sends cross-site without a preflight. The session
cookie's SameSite default already keeps it off such requests, but a
deployment that relaxes it would accept uploads from any page. The route now
applies the same origin rule as CORS before reading the session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(core): load child_process normally in the git checkout reader
readGitCheckout loaded node:child_process through require(), which does
not exist in the ESM API runtime. Linking a project space to a folder
that is a git checkout therefore failed after the space was created.
The lazy load is no longer needed: the reader is only reachable through
the server-only "@rakazo/core/git-checkout" subpath, never through the
package entry the web app bundles. Import spawn at the top instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): ask before discovering projects and let the user choose
The onboarding scan started on its own, discarded what it found, claimed
to read email and calendars, simulated progress for everyone who could
not scan, and its project creation always failed (wrong Space fields and
a space lookup by title across tenants).
Discovery now asks first and creates nothing without a choice:
- The scan step appears only for the deployment owner when
ONBOARDING_SCAN_HOME is set; everyone else skips it. The simulated
scan is gone.
- It shows the folder and asks. Only directory listings are read: git
projects up to four levels deep, their languages and a few tools
(Docker, GitHub Actions, Terraform). Symlinks are never followed and
each chosen folder is resolved again before use, so nothing outside
the folder is reached. Results stay with the account that scanned.
- Found projects are a checklist (none preselected); found languages and
tools are facts (preselected). "Add" creates one project space per
chosen project with createSpaceForMember and wireProjectWorkspace in
the user's own organization, reusing the user's own space already
linked to that folder, and saves the chosen facts as onboarding facts
under the profile lock and fact limit.
- When a connection becomes connected (direct connect, authorization
round trip, or catalog reconciliation), "Uses <App>" is remembered as
a tools fact from the connector. Revoking keeps it.
The E2E harness now points ONBOARDING_SCAN_HOME at a fixture folder and
starts without a deployment owner, so owner-only onboarding steps appear
only in specs that take the seat through /__e2e/deployment-owner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): start exports, transcriptions and cancels from the media card
The card's follow-ups now reach the media job RPCs, and it only offers what
the space can run.
- media.capabilities answers which operations the actor's space can run
(booleans only). The card asks once per minute per space, and only when it
shows finished video or audio, so other cards cost nothing.
- Export and Transcribe call media.jobs.start with the card's message, so the
same bot posts the new card; Cancel calls media.jobs.cancel. A refused
start shows the server's message under the card.
- New files use top-level import type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): GPU guard for smaller cards, top-level type imports
- The GPU guard waited for free VRAM at the operation's floor even on a card
whose total memory is below it (an 8 GB card and a 12 GB video floor), so
such jobs deferred until their budget ran out. The requirement is now capped
at 90% of the card's total; a card with less than half the floor fails at
once with "The GPU does not have enough memory for this job."
- Tests pin that media tools reach a run only through the space's
capabilities and within the bot's allowlist now that the image tool
definitions exist.
- Type-only imports are top-level `import type` in the media runtime, tools,
RPCs and tests (AGENTS.md).
- The ComfyUI settings test gives saveMediaSettings a store with nothing saved,
since the save reads the previous settings before validating.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(media): run video edits and exports through the media runtime
A Postgres suite starts edit_video and export_video through the real
runtime and local artifact store: outputs become linked versions with
posters on the card, exports store one version per preset, appended clips
and subtitle files of another user are refused at start, and an
out-of-range cut fails the job with a readable error. It skips without
Postgres or ffmpeg.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): abandoned handlers cannot crash the worker, sweep stale temp dirs
- A handler that ignores its abort is left behind after the grace period;
a late rejection from it (or from the grace timer after the job settled)
was unobserved, and the worker exits on unhandled rejections. Both sides of
the race are now observed and the grace timer is cleared with the job.
- Ending a job removes its temp directory when no runner in this process
holds it (leftovers of a runner that died here), and reconciliation sweeps
directories of jobs that no longer run.
- An executor test runs a media tool end to end through applyTool: shown
only for operations the space can run, started with the run's scope and
effect key, continue_after split off, refused when not exposed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): offer media card follow-ups on any settled job
Failed or cancelled jobs can still carry finished outputs, so the card asks
for the space's capabilities whenever a settled job shows video or audio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): coach on the Heute page
Heute only listed runs and linked to a hard-coded local Mission Control
address, so the coach's work had nowhere to show.
- Today's brief at the top, habits with a check-off and their streak,
and active goals, each only when there is something to show; then the
existing needs-you, doing and done lists.
- One icon button opens the check-in times (morning, evening, weekly),
loaded when opened and saved on every change.
- The page reads coach.today, refreshes it when a run finishes or the
tab becomes visible, and checks habits off optimistically; the date
header uses the user's time zone from the profile.
- Removes the Mission Control link and its constant.
- Tests: helper unit tests and a web E2E that writes the brief through
the main bot, checks a habit off across a reload and sets check-in
times (screenshots heute-coach and heute-rhythm).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(onboarding): walk the onboarding questions end to end; mobile reads cards as text
- onboarding-intake.spec walks focus, goal (after typing into the composer, which still
works), check-ins, quiet hours and the notifications card when the browser offers it,
with the onboarding-intake-goal, -rhythm and -done screenshots.
- completeOnboarding and the wizard specs drop the preferences step; the scan step is
optional (skipped when it asks, otherwise it advances by itself).
- The user-profile spec now checks the browser-filled profile and the focus fact; the
conversation spec checks the shorter app suggestion and the following goal question.
- The hover spec measures a reply that fills its row (a bot row's rail sits at the row's
end, so the old long onboarding line was what made it pass) and scopes Send to the
composer, since an open goal card has its own.
- Mobile shows a choice card as its question and answer instead of an empty bubble.
- The focus options drop their unused summary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): stub the Notification permission without a static-only class
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): let bots title image edits in the user's language
Edit cards showed English words such as "crop · flip · extend" to every user.
edit_image now takes an optional title written by the bot in the user's
language, used for the card and the file name like edit_video. Without one,
the card falls back to the repaint prompt or to language-neutral tokens only:
sizes, ratios, degrees, arrows and format names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(media): transcribe real clips through ffmpeg
Generated clips (nothing binary committed) go through extractAudio: a video's
audio reaches the server as 16 kHz mono WAV, a 12-second recording is
uploaded in two ranges under a small limit, and a video without sound is
refused before anything is uploaded. Skipped where ffmpeg is missing, which
may include CI runners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): cover group uploads in the main conversation
The image E2E now runs in the main conversation, which is a group thread,
so the browser test covers group uploads and reads from the artifact route
as well as bot threads. Bot file attachments keep their run's operation id
for tracing, and the non-streaming attach cap is the inline cap directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): title transcription cards with what they transcribe
A card that only says "Transcript" does not tell two transcriptions apart,
and it is English whatever the user's language.
- transcribe takes an optional title like the video operations, so a bot
names the card in the user's language; "Transcript" stays the default.
- Transcribe on a media card passes the source file name as the title.
- The E2E seeds the card through bootstrap as well as threads/get, since the
bot thread's first snapshot arrives with bootstrap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(web): match the media player stand-in to the pipeline's props
The pipeline workstream's MediaPlayer takes artifactId, kind, name, poster
and size, without a MIME type; the card and the stand-in now use exactly
that, so its file replaces the stand-in without other changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): media settings E2E with the media-settings screenshot
The Media settings section (added in the previous web commit) lets the
deployment owner point media at their own servers: ComfyUI URL and optional
key, with the image model and custom image/video workflows folded under
Advanced, and a transcription URL with optional model and key. Saved keys are
never shown again; the key field says one is saved and offers to remove it.
Members who do not own the deployment do not see the section.
The spec fills both servers, captures `media-settings`, saves and checks the
exact update the server receives, then removes the saved key and the
transcription server; a second spec checks a member has no Media section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): size uploaded videos from their own frame
Uploaded videos carry no dimensions, so a vertical clip sat letterboxed in
a 16:9 frame. The player now takes the frame size from the file's metadata
when the block has none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): disclose the transcription server before dictation uses it
Dictation falls back to the owner's transcription server when the voice
provider cannot transcribe, so that server is a voice recipient too: it is
listed for voice use (as "Transcription (<origin>)") whenever it would take
the recording, and not when the voice provider transcribes itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): render media card outputs with the URL-based previews
The media pipeline moved image and file previews to the artifact route, so
they no longer take a bot or group target. The media card still passed one,
which no longer type-checks; only its follow-up actions need the target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(api): let large uploads finish and abort stalled ones
Node's default 5-minute requestTimeout cut off a 500 MiB video upload on
any uplink slower than about 14 Mbit/s, so the video cap was unusable over
ordinary connections.
- The API server's requestTimeout is 30 minutes, enough for the largest
upload at about 2.5 Mbit/s. headersTimeout keeps its 60-second default.
- Each streamed upload aborts when the client sends no bytes for 60
seconds: the route answers 408, the partial file is deleted and no row
is written, so a slow-drip client cannot hold a connection for the full
request timeout. Time spent writing to disk does not count.
- The store and the upload guard no longer wait for a body to confirm its
cancellation, which a stalled or hostile client may never do.
- A guard test checks that every file the executor attaches records the
thread's group, which the render_plot chart fallback once dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): use snake_case keys in every media tool input
The model saw edit_video and export_video with artifact_id next to
generate_image, generate_video, edit_image and transcribe with artifactId,
sourceArtifactId and negativePrompt. Mixed conventions in one tool list invite
wrong calls, so all media inputs now use snake_case like continue_after and the
other builtin tools: artifact_id, source_artifact_id, negative_prompt,
duration_seconds. Handlers, artifact checks, the media card's transcribe
action and the tests follow. Nothing has shipped, so stored job inputs need no
migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): act on media cards in their own space, not the selected one
The main conversation lives in the home space, but the user can have another
space selected while it is on screen, and the card's actions sent that space:
media.jobs.start resolved the conversation in the selected space and failed,
and media.jobs.get/cancel only found jobs of the selected space.
The server now takes the space from the card itself. start finds the card's
message in one of the actor's own threads (the conversation named by botId or
groupId), requires the actor to still be a member of that thread's space, and
resolves the conversation and the source artifacts there. get and cancel find
the actor's job by id and require membership of the job's space.
media.capabilities takes an optional job id and answers for that job's space;
the card asks through its job and shares the answer per conversation. Every
other case is a 404, as before.
A Postgres test runs a card in the home-space main conversation while another
space is selected (start, get, capabilities, cancel) and checks that another
user and a user who left the card's space get 404 on every procedure.
This also completes the media settings section committed earlier as work in
progress (01eea83); its code was final, only that commit message said
otherwise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): find the sidebar Today button by its English label
The shell contrast test added on main looks up the sidebar button by the
old German source text, which this branch replaces with "Today".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* style: use top-level type imports in the media changes
AGENTS.md asks for top-level `import type` instead of inline `type`
specifiers; this applies it to the import statements the media work added
or changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore(web): extract catalogs and translate the media strings to German
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): show the onboarding cards and check-in names in the user's language
The onboarding cards, the closing line and the coach routine names were English server text
for every user. The server keeps the state and an English fallback; the web now words them
through Lingui, so there is one translation source.
- Choice cards: the web says the question, the option labels and the placeholder from the
step and option ids, and keeps the server text for anything it does not know (quiet-hour
ranges are already written in the user's locale). Another bot's focus card is recognized
by its option ids. Mobile and agent history keep the server text.
- The "Got it. … are a good place to start." line is gone: the app cards speak for
themselves, and with no cards nothing is said.
- The closing line becomes an intake_note block (note "done", nextCheckInAt, English text).
The web words it with the first check-in in the user's clock; mobile blockText and
blocksToAgentHistoryText read its text through their generic fallback.
- Routines carry coachKey. The routine list and editor show a check-in under its label in
the UI language until the user renames it; saving an untouched label keeps the stored
name. The English names move to contracts (COACH_ROUTINE_NAMES); prompts stay English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* style: use top-level type imports in the coach changes
AGENTS.md asks for top-level `import type` instead of inline `type`
specifiers; this applies it to the import statements the coach work added
or changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore(web): extract catalogs and translate the coach strings to German
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): look for the bot reply in the transcript only
The main conversation's sidebar row now previews the latest reply, so the
unscoped text lookup matched the sidebar too and failed in strict mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): drop the trading status from the shell contrast test
main's contrast test now waits for the sidebar trading status before
measuring; this branch removes that status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(onboarding): link the foreign folder while its tenant owns the deployment
Since #112 only the deployment owner can link a host folder, so the
discovery test creates the other tenant's linked space while that tenant
holds the deployment, then hands the deployment to the scanning owner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore(web): extract catalogs and translate the in-app browser strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): write poster frames as full-range JPEG
Newer ffmpeg releases refuse to encode JPEG from limited-range video
frames, so poster frames failed on CI's ffmpeg while older versions
accepted the same command. The poster is converted to full-range
yuvj420p before encoding, which every supported ffmpeg version takes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: look up the home main conversation in the scheduling journey
Since every project has its own main conversation, the journey that
schedules for a specialist found the user's main conversation with
findFirst over all of them. With the Lab project's conversation also
matching, it picked either one, so the check failed about one run in
three. The lookup now names the home space, where the specialist joined
and the routine is created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: look up the home main conversation in the scheduling journey
Since every project has its own main conversation, the journey that
schedules for a specialist found the user's main conversation with
findFirst over all of them. With the Lab project's conversation also
matching, it picked either one, so the check failed about one run in
three. The lookup now names the home space, where the specialist joined
and the routine is created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: expect the home main conversation's ask in another space's inbox
Every project now has a main conversation of its own, so an ask waiting in
the home main conversation is not visible from a project and its inbox
lists it, answered in the home space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(coach): move check-ins when the user's time zone changes
The coach check-ins are main-bot routines scheduled in the profile time zone, but a later
zone change (About you, onboarding, the main bot's update_user_profile tool, or forgetting
the whole profile) left them in the old zone until the rhythm was saved again, so they fired
at the wrong local time.
syncCoachTimeZone, next to the rhythm code, moves the main bot's coach routines that are not
in the current profile zone into it, recomputes the next run of armed ones and re-arms their
wakeup. Paused check-ins move but stay paused, routines the user made are never touched, users
without check-ins get none, and a save that leaves the zone as it is changes nothing. The
profile update and forget RPCs and the profile tool call it whenever they write the zone.
Quiet hours, the daily nudge cap and coach dates are evaluated in the profile zone when they
are used, so they need no migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): word the main conversation preview in the user's language during onboarding
While onboarding wrote the main conversation, its sidebar row, project rows and Heute showed the
server preview: nothing for a pending card (a card has no text of its own, so the row fell back
to "Ready") and the English fallback of the closing note, even for a German user.
Groups now carry `previewBlock` next to `preview`: the latest message when it is an onboarding
card or note, validated with the same block schemas the messages use (now named exports). The
web words it with the helpers that render the card and the note, so no copy is duplicated: the
question while a card waits, the answer once given, and the closing note in the user's language
and clock. A dismissed card keeps the plain preview, as the conversation no longer shows it.
The field is left out for every other message, and mobile keeps using the plain `preview`.
The chosen-answer lookup moves from the card into onboarding-copy so the card and the preview
share it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(media): stream large media between the store and local computers
Attachments copied into a local computer's workspace were read whole from
the artifact store first, so a 500 MiB video sat in memory before it was
written to disk. attach_file did the same in the other direction: a large
file on a local computer was read whole before it was stored.
SandboxProvider gains optional readFileStream/writeFileStream. The desktop
(host workspace) provider implements them with the same path confinement
as its byte methods, one chunk in memory at a time, and removes a file
whose write fails or is aborted, including one that replaced an earlier
copy. The host-aware wrapper routes them, and providers without them
(remote computers, still capped at 12 MiB) receive the collected bytes.
Current-turn staging and attach_file now go through these helpers, and
unread streams are cancelled so the stored file is released. Media job
inputs stop copying when the job is cancelled.
The stream helpers move from the local artifact store into a shared
module so both use one implementation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(media): live ComfyUI progress over its websocket
A video generation runs a single ComfyUI prompt for minutes, and the media
card only advanced after each prompt finished, so it sat still until the
clip was done.
The ComfyUI provider now follows ComfyUI's standard /ws stream while its
prompts run and maps step progress to the job's overall fraction across
all prompts: never backwards, and short of each prompt's share until its
outputs are downloaded. The history poll stays the source of truth for
completion and errors; a socket that cannot connect, drops, or stays
silent leaves generation exactly as before, and it is closed on finish,
failure, cancel and abort. Each generation uses its own client id, so
concurrent jobs do not take over each other's messages.
The socket goes through a new createOpenAiCompatibleWebSocket next to the
guarded fetch: same URL and key checks, same resolved-address lookup,
ws/wss derived from http/https, the same Authorization header, a message
size cap, and a hard drop when a server never finishes the closing
handshake. It uses the undici package's WebSocket, so no dependency was
added. A provider given a replaced fetch without a matching socket opens
none, so tests and emulators never reach the network.
The ComfyUI emulator now serves the websocket and reports progress for
running prompts; provider, guard and conformance tests cover ordering,
multi-prompt mapping, unavailable and failing sockets, stray messages,
closing on cancel, and the address checks against a loopback server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): look for the focus question in the conversation, not the sidebar
The main conversation's sidebar row now shows the pending intake question,
so a page-wide text match finds it twice (strict mode) or never reaches zero.
The specs mean the card in the open conversation, so they look there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
---------
Co-authored-by: Claude <noreply@anthropic.com>
Bot settings could export a bot as JSON, but nothing could read that file
back, so an export was not a way to recover or move a bot.
bots.import creates a new bot from an export manifest: profile and
instructions, bot memory, home files and routines. Team Computer paths
are rewritten to the new bot's workspace, unsafe paths reject the whole
import, imported routines start paused and one-shot schedules are left
out. History is not replayed; the bot starts its own conversation. A
failed import removes the half-created bot.
The New bot panel gets an import button that takes the exported file.
Claude-Session: https://claude.ai/code/session_01Sbaz56zNFZBJ1whrpasMMs
Co-authored-by: Claude <noreply@anthropic.com>
* chore: remove legacy orchestrator, trading and one-off script leftovers
The repository still carried the pre-evOo prototype: a Python orchestrator,
the old HTML "evoo-platform" cockpit (whose test ran in the root unit
suite), an apex-trading package, the hot-reload deploy.sh for it, a
permissions manifest, an architecture note with local file links, agent
handoff notes, a Steve Jobs persona template and about ninety one-off
scripts that probe a developer database with hard-coded local URLs. None
of it is used by the product, CI or the real deploy in ops/evo-deploy.
Delete those files, drop the evoo-platform test glob from the root vitest
config, the Cockpit section from .env.example and the dead biome and
gitignore entries, and update the docs that listed the removed tools.
scripts/start-tmux.sh no longer starts the removed sentinel and resolves
the checkout from its own location instead of a machine-specific path;
scripts/seed-ollama.ts requires DATABASE_URL instead of falling back to a
local URL with a username.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): drop trading and persona prompts from the conversation shell
The web shell still showed leftovers from the trading prototype: composer
chips with German trading and "Steve Jobs" prompts, trading suggestion
chips under the empty conversation line, a German trading hero for the
empty transcript, and a German "data protection" status panel in the
sidebar. They suggested actions the product does not have and added
persistent chrome.
Remove the composer chips and their helper, the empty-state chips and the
hero (the empty conversation keeps its existing one-line start hint), the
sidebar trading indicator and the data trust panel. The new-bot E2E now
asserts the plain empty state; the sidebar spec no longer expects the
trading status, and the placeholder Steve Jobs spec with no assertions is
gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor: remove the trading bot status integration
With the sidebar trading indicator gone, the only remaining consumer of
the trading bot status was a banner in General settings that reused the
same component. evOo is a personal assistant; a status proxy and poller
for one external trading service, configured through service-specific
TRADING_BOT_URL and VITE_TRADING_BOT_URL variables, does not belong in
the core product.
Remove the settings banner and its prop plumbing, the web poller, the
indicator component, the /api/trading-bot/status proxy, the shared core
contract, the env loading and both variables from .env.example, together
with their tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore: remove one-off database scripts and a stale review note
packages/db carried four hand-run scripts outside the package build that
query or seed a developer database with a hard-coded connection string,
one of them for a specific account. docs/VOICE-FIRST-REVIEW.md was a
one-time verification note for a workflow that no longer exists and
named private machines and local paths. Nothing references them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(memory): remember appends dated notes instead of replacing memory
The remember tool rewrote the bot's whole MEMORY.md with each call, so every
new fact erased the earlier ones unless the model re-sent the full document.
- MemoryCommitRequest gains mode "append" (the store joins the note onto the
current content inside the same serializable transaction and still writes a
revision row) and every commit is capped at 16 KB per document.
- remember appends a dated bullet ("- YYYY-MM-DD: ...", the user's local date)
by default and rewrites only with mode "replace"; a full document returns an
error that tells the bot to condense it. Retries of the same tool call stay
single appends through the existing effect record.
- Scripted runtime memory entries can append too; the scripted "remember"
script now appends instead of overwriting the bot's memory.
- When memory does not fit the run's 32 KB budget, the prompt keeps a
document's newest lines instead of its oldest.
- memory.update maps an oversized save to BAD_REQUEST.
- docs/PRODUCT.md records that UserFact is the cross-space user model and the
space MEMORY.md stays space notes (no migration).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(coach): goals, habits, check-ins, daily briefs and check-in rhythm
The main agent should coach the user, but nothing stored what the user
wants to achieve or keep doing, and no routine ever checked in. This adds
the data model and the interfaces the onboarding intake and the coach
tools build on.
- Schema: UserGoal, Habit, CheckIn (one per habit, goal or day and date)
and DailyBrief, all keyed to the user's local calendar date, plus a
coachKey on Routine that marks the main bot's coach check-ins.
- Contracts (coach.ts): CoachRhythm, Goal, Habit, CheckIn, DailyBrief and
CoachToday DTOs and the RPC inputs.
- Repository (@rakazo/db coach.ts): goal, habit and check-in CRUD with
per-user limits, upsertDailyBrief and loadCoachToday with streaks in
the user's time zone.
- coach-rhythm.ts: setCoachRhythm/getCoachRhythm keep up to three main
bot routines (morning brief, evening check-in, weekly review) in the
main conversation in step with the user's chosen times.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(notifications): add push subscriptions, notification settings and web push interfaces
Push could only reach one mobile device per user (a token file), quiet hours did not exist,
and the web app had no service worker, so a closed tab never heard about finished work.
This lands the shared interfaces first so onboarding can build on them:
- push_subscriptions (web push endpoints and Expo tokens, one row per device) and
user_notification_settings (quiet hours as minutes in the profile time zone, nudges, a
per-day nudge counter), with CHECK constraints.
- NotificationSettingsSchema and web push contracts, notifications.settings.get/update and
notifications.webPush.config/subscribe/unsubscribe (owner-scoped, https endpoints only).
- getNotificationSettings/updateNotificationSettings plus the policy and nudge-cap helpers.
- VAPID keys from VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY or generated once and stored encrypted,
and an SSRF-guarded web push sender built on the web-push package.
- The web app links its manifest, registers a push-only service worker where supported (secure
context, not the desktop app) and exposes webPushSupported/webPushEnabled/enableWebPush/
disableWebPush.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore: replace machine-specific paths and addresses with placeholders
ORIGIN.md pinned the intended checkout to an absolute path under a
personal home directory, docs/LOCAL-SETUP.md listed folders on one
developer's machine, and two tests used what looks like a real tailnet
address. Use a home-relative path, drop the machine-specific folder list,
and use 100.100.100.100 in the tests; it is still inside 100.64.0.0/10,
so both tests check the same thing as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(coach): repository, streak and rhythm suites
Covers what the coach interfaces promise before anything builds on them:
goal and habit ownership and limits, one check-in per habit, goal and day
dated in the user's time zone, streaks across weekends and weeks, and a
rhythm that keeps exactly one main-bot routine per check-in and is
idempotent when set again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(contracts): share one clock-time schema
The coach rhythm and the quiet hours both parse "HH:MM" wall-clock times
and each defined its own ClockTimeSchema, which the contracts index cannot
export twice. Both now import it from one module.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): ask the onboarding questions in the main conversation
The wizard asked for the account name again and a "Tell us about yourself" form, and the
focus answer in the main conversation was never saved. The main bot now gets to know the
user in the main conversation instead, one card after each answer.
- UserOnboarding keeps the pending step, the focus and when onboarding finished.
- onboarding-intake.ts declares the steps (focus, goal, rhythm, quiet hours, notifications)
as cards plus what their answers change; onboarding.ts commits an answer, the focus app
suggestions and the next card or the closing line in one transaction.
- The choice block gains a step, an optional text input and the typed answer; choose takes
the step, the text and whether the device can still turn push on.
- The focus answer becomes a goals fact; the goal, rhythm and quiet-hours effects and the
push seam are placeholders until the coach and notification modules are merged.
- The wizard drops the preferences step and fills time zone, locale and language from the
browser; the main bot's prompt notes open onboarding cards; agent history shows a card's
question and answer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(notifications): fan push out to every device, honour quiet hours, add notify_user
Run notifications reached at most one phone and nothing could hold them back at night.
- PushNotificationProvider sends each notification to all of the user's registered devices
(web push and Expo), skips everything during quiet hours in the profile time zone, drops
devices the push service reports gone (404/410, DeviceNotRegistered), records lastUsedAt and
links each push to its conversation.
- Expo tokens move from DATA_DIR/push-tokens files into push_subscriptions: the API imports and
deletes the legacy files at start; notifications.registerPush/unregisterPush keep their
contract for the mobile app (tokens must look like Expo tokens).
- NotificationMessage gains the nudge kind, and the main bot gets notify_user: a short push to
the main conversation that respects the nudge switch, quiet hours and a cap of three per day.
It is withheld from shared messaging channels and from runs answering another owner's agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(coach): main-bot coach tools and goals in its prompt
The main agent had no way to record what the user wants to achieve or how
their day went, and its prompt never showed their goals or habits.
- update_goal, update_habit, log_check_in and write_daily_brief, main-bot
only and, like update_user_profile, withheld from shared channels and
from runs that answer another owner's agent. Goals and habits can be
named by id or exact title.
- The main bot's own runs list active goals and habits (with ids, done
state and streak) in the <user> block; the basics-only block and every
other bot never see them. The main agent role adds a short coaching
stance and names each coach tool only when the run has it, so the
check-in routine prompts stay tool-agnostic.
- The scripted runtime understands a few coach commands and the morning
check-in, so tests can drive the tools end to end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): use English Lingui sources for German UI text
Several web surfaces hard-coded German (the project card, roster and
activity status labels, the sidebar "Heute" label) or used German text as
the Lingui source message (sidebar sections, project timeline, bot picker,
voice entry, group stage, copy and read-aloud buttons). English users saw
German, and because production builds keep only message ids, none of these
strings could be translated per locale.
Convert them to English source messages, keeping the meaning:
- ProjectCreatedCard: Lingui Trans/Plural instead of literals.
- agent-activity: labels go through i18n._ with literal ids so the module
stays importable from plain unit tests; the extractor still collects them.
- Shell, ProjectSessionTimeline, bot-picker, StageVoiceEntry, OoStage:
English sources, reusing existing messages where one already fit
("just now", "{n}m ago", "Search", "No bots match", "Set up voice",
"Group conversation", "Transcript").
- The conversation picker matches its create entry against the translated
label instead of a fixed German string.
The German wording returns through the de catalog in the next commit.
Unit and E2E tests now assert the English labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): keep Lingui catalogs complete and in sync
The committed catalogs were last extracted weeks ago: about 330 source
messages were missing from every locale, and production builds keep only
message ids, so those strings rendered as hash ids (sidebar sections,
privacy settings, profile, timeline, ...). German also had 226 empty
entries, and every extraction rewrote the files because references carried
line numbers.
- lingui.config.ts: po references without line numbers, and a stable sort
that breaks Lingui's ordering ties (explicit ids, equal text) by id, so
the output no longer depends on file-system order.
- Re-extract all catalogs with --clean (drops 25-44 obsolete entries per
locale; no existing translation changed).
- Translate every German entry (du form, consistent with the existing
catalog). Other locales keep empty entries and fall back to English.
- i18n-catalog-sync.test.ts extracts into a copy of the catalogs and fails
when the committed files differ or a German entry is empty, so stale
catalogs fail the unit test job (offline, about 5 s).
Adds @lingui/format-po (already installed through @lingui/cli) as a direct
dev dependency to configure the formatter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(memory): derive the run's memory budget from the document limit
The prompt budget was a separate 32 KB constant; it is now twice the
per-document limit, so a full bot document and a full space document keep
fitting if either number changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(profile): edit facts in place in About you
About you could only confirm or delete what the bots know about the user, so a
fact that was slightly wrong or filed under the wrong category had to be
deleted and taught again, and a fact could not be limited to some spaces from
the app even though the profile supports it.
- Each fact gets an Edit button that turns its row into an inline editor:
wording, category, and (when the user has more than one space, or the fact
is already limited) which spaces it shows in. Escape or Cancel leaves it
unchanged; only changed fields are sent through profile.update editFacts.
- A limited fact names its spaces next to its source.
- The repository validates edits like new facts: a wording whose normalized
form is empty or equals another fact of the user is refused, all edits of
one update apply atomically, and the stored embedding is only dropped when
the wording really changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(inbox): list and answer items from all of the user's spaces
The inbox in the main conversation only showed asks, approvals, takeovers and
unread threads of the current space. The main conversation lives in the home
space, so anything a bot needed in a project space never reached it.
- inbox.list now covers every space the user belongs to (except spaces being
deleted), still only the user's own bots, runs and threads. Items from
another space carry that space's id and name.
- The web inbox shows the space name next to the bot for those items, answers
them with that space's header so the existing thread procedures check
membership and ownership for that space, and opens their thread in it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(coach): coach RPCs for goals, habits, check-ins, today and rhythm
The web app needs to show and change the user's coach data, and the
check-in times need a way to change after onboarding.
- coach.today, coach.goals.*, coach.habits.*, coach.checkIns.* and
coach.rhythm.get/set, owner-only and not space-scoped; another user's
ids read as not found, invalid input as a bad request.
- coach-rhythm reuses the shared clock-time helpers.
- Tests: an API suite covering ownership and validation for every
procedure, the coach tools driven through the main bot (and refused
for a specialist), and a morning check-in routine that fires in the
main conversation and writes today's brief; the unauthenticated
authorization sweep lists the new procedures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): save goal, check-in rhythm, quiet hours and push from the cards
With the coach and notification modules merged, the onboarding answers now take effect:
- the goal card creates a goal (source onboarding) through createGoal;
- the rhythm card sets the main bot's check-in routines through setCoachRhythm, and the
closing line names the first check-in from the routines that were saved;
- the quiet-hours card updates the notification settings;
- the choice card tells the server whether this browser can still turn push on, so the
notifications card is offered only there, and "Turn on" subscribes before answering.
The thread lock becomes NO KEY UPDATE so a rhythm routine, which references the thread, can
be written while the answer holds the lock. The goal answer is capped at the coach title
length. A Postgres suite walks the questions end to end: answers, skips, a dismissed card, a
stale and a doubled answer, another bot's focus card, and another user's access.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(notifications): cover quiet hours, delivery, nudges and the notification RPCs
The quiet-hours CHECK accepted a row with only one bound set, because a NULL comparison made
the whole condition NULL; it now requires both bounds explicitly. The migration has not shipped
yet, so it is corrected in place.
- Unit: quiet-hours math across midnight, time zones and daylight saving; VAPID key handling
(configured, generated once and shared, invalid); the web push sender (encrypted VAPID-signed
request, gone on 404/410, no private or non-https endpoints); the fan-out with fake senders.
- PostgreSQL: settings and subscription repositories (constraints, per-user removal, endpoint
hand-over, device cap, concurrent nudge cap, cascade on account deletion), delivery and
notify_user against a real main conversation, and every notification RPC through the API
including cross-user isolation and endpoint validation.
- .env.example documents the optional VAPID keys and VAPID_SUBJECT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): notification settings, and sign-out stops this browser's push
Users had no way to turn on push for a browser, set quiet hours or opt out of nudges.
Settings gains a compact Notifications section: a switch for push on this device (only where
web push works: a secure context outside the desktop app), quiet hours with from/to times
(22:00-07:00 when first turned on) and a switch for nudges. Changes save in order as they are
made. A browser that blocked notifications says so instead of offering a dead switch. Logging
out unregisters the browser first, so a shared computer stops receiving the account's pushes.
The E2E stubs the Push API (headless Chromium has no push service), walks all three controls,
checks they survive a reload and captures the notification-settings screenshot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(profile): keep the fact editor's Save label visible
Small buttons in ui-web merge their text-ui-label size over the default
variant's text color, so the small Save button rendered ink on ink. The
editor now uses default-size controls, with the category select at the same
height.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* refactor(notifications): keep the push sender contract next to the fan-out
The payload, target and outcome types are shared by the Expo and web push senders, so they now
live with PushSender in push-notifications.ts instead of in the web push adapter. The settings
E2E also stubs the Notification permission (headless Chromium reports it as denied even when
granted), and the quiet-hours inputs are wide enough for 12-hour times.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(core): load child_process normally in the git checkout reader
readGitCheckout loaded node:child_process through require(), which does
not exist in the ESM API runtime. Linking a project space to a folder
that is a git checkout therefore failed after the space was created.
The lazy load is no longer needed: the reader is only reachable through
the server-only "@rakazo/core/git-checkout" subpath, never through the
package entry the web app bundles. Import spawn at the top instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): ask before discovering projects and let the user choose
The onboarding scan started on its own, discarded what it found, claimed
to read email and calendars, simulated progress for everyone who could
not scan, and its project creation always failed (wrong Space fields and
a space lookup by title across tenants).
Discovery now asks first and creates nothing without a choice:
- The scan step appears only for the deployment owner when
ONBOARDING_SCAN_HOME is set; everyone else skips it. The simulated
scan is gone.
- It shows the folder and asks. Only directory listings are read: git
projects up to four levels deep, their languages and a few tools
(Docker, GitHub Actions, Terraform). Symlinks are never followed and
each chosen folder is resolved again before use, so nothing outside
the folder is reached. Results stay with the account that scanned.
- Found projects are a checklist (none preselected); found languages and
tools are facts (preselected). "Add" creates one project space per
chosen project with createSpaceForMember and wireProjectWorkspace in
the user's own organization, reusing the user's own space already
linked to that folder, and saves the chosen facts as onboarding facts
under the profile lock and fact limit.
- When a connection becomes connected (direct connect, authorization
round trip, or catalog reconciliation), "Uses <App>" is remembered as
a tools fact from the connector. Revoking keeps it.
The E2E harness now points ONBOARDING_SCAN_HOME at a fixture folder and
starts without a deployment owner, so owner-only onboarding steps appear
only in specs that take the seat through /__e2e/deployment-owner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): coach on the Heute page
Heute only listed runs and linked to a hard-coded local Mission Control
address, so the coach's work had nowhere to show.
- Today's brief at the top, habits with a check-off and their streak,
and active goals, each only when there is something to show; then the
existing needs-you, doing and done lists.
- One icon button opens the check-in times (morning, evening, weekly),
loaded when opened and saved on every change.
- The page reads coach.today, refreshes it when a run finishes or the
tab becomes visible, and checks habits off optimistically; the date
header uses the user's time zone from the profile.
- Removes the Mission Control link and its constant.
- Tests: helper unit tests and a web E2E that writes the brief through
the main bot, checks a habit off across a reload and sets check-in
times (screenshots heute-coach and heute-rhythm).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(onboarding): walk the onboarding questions end to end; mobile reads cards as text
- onboarding-intake.spec walks focus, goal (after typing into the composer, which still
works), check-ins, quiet hours and the notifications card when the browser offers it,
with the onboarding-intake-goal, -rhythm and -done screenshots.
- completeOnboarding and the wizard specs drop the preferences step; the scan step is
optional (skipped when it asks, otherwise it advances by itself).
- The user-profile spec now checks the browser-filled profile and the focus fact; the
conversation spec checks the shorter app suggestion and the following goal question.
- The hover spec measures a reply that fills its row (a bot row's rail sits at the row's
end, so the old long onboarding line was what made it pass) and scopes Send to the
composer, since an open goal card has its own.
- Mobile shows a choice card as its question and answer instead of an empty bubble.
- The focus options drop their unused summary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): stub the Notification permission without a static-only class
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): find the sidebar Today button by its English label
The shell contrast test added on main looks up the sidebar button by the
old German source text, which this branch replaces with "Today".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(onboarding): show the onboarding cards and check-in names in the user's language
The onboarding cards, the closing line and the coach routine names were English server text
for every user. The server keeps the state and an English fallback; the web now words them
through Lingui, so there is one translation source.
- Choice cards: the web says the question, the option labels and the placeholder from the
step and option ids, and keeps the server text for anything it does not know (quiet-hour
ranges are already written in the user's locale). Another bot's focus card is recognized
by its option ids. Mobile and agent history keep the server text.
- The "Got it. … are a good place to start." line is gone: the app cards speak for
themselves, and with no cards nothing is said.
- The closing line becomes an intake_note block (note "done", nextCheckInAt, English text).
The web words it with the first check-in in the user's clock; mobile blockText and
blocksToAgentHistoryText read its text through their generic fallback.
- Routines carry coachKey. The routine list and editor show a check-in under its label in
the UI language until the user renames it; saving an untouched label keeps the stored
name. The English names move to contracts (COACH_ROUTINE_NAMES); prompts stay English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* style: use top-level type imports in the coach changes
AGENTS.md asks for top-level `import type` instead of inline `type`
specifiers; this applies it to the import statements the coach work added
or changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore(web): extract catalogs and translate the coach strings to German
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): look for the bot reply in the transcript only
The main conversation's sidebar row now previews the latest reply, so the
unscoped text lookup matched the sidebar too and failed in strict mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): drop the trading status from the shell contrast test
main's contrast test now waits for the sidebar trading status before
measuring; this branch removes that status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(onboarding): link the foreign folder while its tenant owns the deployment
Since #112 only the deployment owner can link a host folder, so the
discovery test creates the other tenant's linked space while that tenant
holds the deployment, then hands the deployment to the scanning owner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore(web): extract catalogs and translate the in-app browser strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: look up the home main conversation in the scheduling journey
Since every project has its own main conversation, the journey that
schedules for a specialist found the user's main conversation with
findFirst over all of them. With the Lab project's conversation also
matching, it picked either one, so the check failed about one run in
three. The lookup now names the home space, where the specialist joined
and the routine is created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: expect the home main conversation's ask in another space's inbox
Every project now has a main conversation of its own, so an ask waiting in
the home main conversation is not visible from a project and its inbox
lists it, answered in the home space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(coach): move check-ins when the user's time zone changes
The coach check-ins are main-bot routines scheduled in the profile time zone, but a later
zone change (About you, onboarding, the main bot's update_user_profile tool, or forgetting
the whole profile) left them in the old zone until the rhythm was saved again, so they fired
at the wrong local time.
syncCoachTimeZone, next to the rhythm code, moves the main bot's coach routines that are not
in the current profile zone into it, recomputes the next run of armed ones and re-arms their
wakeup. Paused check-ins move but stay paused, routines the user made are never touched, users
without check-ins get none, and a save that leaves the zone as it is changes nothing. The
profile update and forget RPCs and the profile tool call it whenever they write the zone.
Quiet hours, the daily nudge cap and coach dates are evaluated in the profile zone when they
are used, so they need no migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): word the main conversation preview in the user's language during onboarding
While onboarding wrote the main conversation, its sidebar row, project rows and Heute showed the
server preview: nothing for a pending card (a card has no text of its own, so the row fell back
to "Ready") and the English fallback of the closing note, even for a German user.
Groups now carry `previewBlock` next to `preview`: the latest message when it is an onboarding
card or note, validated with the same block schemas the messages use (now named exports). The
web words it with the helpers that render the card and the note, so no copy is duplicated: the
question while a card waits, the answer once given, and the closing note in the user's language
and clock. A dismissed card keeps the plain preview, as the conversation no longer shows it.
The field is left out for every other message, and mobile keeps using the plain `preview`.
The chosen-answer lookup moves from the card into onboarding-copy so the card and the preview
share it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): look for the focus question in the conversation, not the sidebar
The main conversation's sidebar row now shows the pending intake question,
so a page-wide text match finds it twice (strict mode) or never reaches zero.
The specs mean the card in the open conversation, so they look there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(web): use English Lingui sources for German UI text
Several web surfaces hard-coded German (the project card, roster and
activity status labels, the sidebar "Heute" label) or used German text as
the Lingui source message (sidebar sections, project timeline, bot picker,
voice entry, group stage, copy and read-aloud buttons). English users saw
German, and because production builds keep only message ids, none of these
strings could be translated per locale.
Convert them to English source messages, keeping the meaning:
- ProjectCreatedCard: Lingui Trans/Plural instead of literals.
- agent-activity: labels go through i18n._ with literal ids so the module
stays importable from plain unit tests; the extractor still collects them.
- Shell, ProjectSessionTimeline, bot-picker, StageVoiceEntry, OoStage:
English sources, reusing existing messages where one already fit
("just now", "{n}m ago", "Search", "No bots match", "Set up voice",
"Group conversation", "Transcript").
- The conversation picker matches its create entry against the translated
label instead of a fixed German string.
The German wording returns through the de catalog in the next commit.
Unit and E2E tests now assert the English labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* feat(web): keep Lingui catalogs complete and in sync
The committed catalogs were last extracted weeks ago: about 330 source
messages were missing from every locale, and production builds keep only
message ids, so those strings rendered as hash ids (sidebar sections,
privacy settings, profile, timeline, ...). German also had 226 empty
entries, and every extraction rewrote the files because references carried
line numbers.
- lingui.config.ts: po references without line numbers, and a stable sort
that breaks Lingui's ordering ties (explicit ids, equal text) by id, so
the output no longer depends on file-system order.
- Re-extract all catalogs with --clean (drops 25-44 obsolete entries per
locale; no existing translation changed).
- Translate every German entry (du form, consistent with the existing
catalog). Other locales keep empty entries and fall back to English.
- i18n-catalog-sync.test.ts extracts into a copy of the catalogs and fails
when the committed files differ or a German entry is empty, so stale
catalogs fail the unit test job (offline, about 5 s).
Adds @lingui/format-po (already installed through @lingui/cli) as a direct
dev dependency to configure the formatter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): find the sidebar Today button by its English label
The shell contrast test added on main looks up the sidebar button by the
old German source text, which this branch replaces with "Today".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore(web): extract catalogs and translate the in-app browser strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: look up the home main conversation in the scheduling journey
Since every project has its own main conversation, the journey that
schedules for a specialist found the user's main conversation with
findFirst over all of them. With the Lab project's conversation also
matching, it picked either one, so the check failed about one run in
three. The lookup now names the home space, where the specialist joined
and the routine is created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(ui-web): teach cn() the design system's text and control scales
cn() used plain tailwind-merge, which only knows Tailwind's default scales.
It read the custom font sizes (text-ui-label, text-ui-caption, ...) as text
colors, so of a size and a color in one class list only the last survived:
- Small, extra-small and large buttons lost their variant's text color. A
small primary button rendered foreground text on the primary fill (ink on
ink in the light theme, near-white on white in the dark theme), and
destructive, secondary and link buttons lost their colors the same way.
- Default-size and icon buttons, tab triggers and the typography helpers
lost their font size and inherited the parent's instead.
The control and icon spacing scales (h-control-sm, size-icon-md, ...) were
unknown as well, so they never merged with other sizes: an explicit h-auto
or h-9 on a Button, Input or Toggle lost to the component's own height, and
large buttons and toggles drew their icons at the medium size.
cn() now extends tailwind-merge with the text and spacing keys styles.css
adds under @theme. A unit test probes cn() with every @theme key in
styles.css, so a new key that is not registered fails the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: look up the home main conversation in the scheduling journey
Since every project has its own main conversation, the journey that
schedules for a specialist found the user's main conversation with
findFirst over all of them. With the Lab project's conversation also
matching, it picked either one, so the check failed about one run in
three. The lookup now names the home space, where the specialist joined
and the routine is created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
---------
Co-authored-by: Claude <noreply@anthropic.com>
* perf(api): filter peer-run activity in the transcript page query
The transcript loaded a raw page, dropped hidden peer-run rows in memory and
rescanned when nothing visible was left, so a long delegated stretch cost two
queries per hidden page. The page query now joins runs and excludes hidden
peer activity in Postgres, so every page is filled with visible rows. The
kept block kinds live in one list shared by paging, the in-memory filter and
live event forwarding.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTJRkJrXAS9e3aEFcFYTy8
* test: port the main-conversation journey fix from #126
Journey 5c fails on main since every space got its own main conversation:
the lookup picked the Lab conversation instead of the home one. Same change
as #126 (including its chart-links warm-up), so this PR can go green; it
no-ops once #126 lands.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTJRkJrXAS9e3aEFcFYTy8
---------
Co-authored-by: Claude <noreply@anthropic.com>
* ops: build the web app in the Evo deploy and add a preview-capable stack unit
The Evo deploy restarted the dev stack without ever building the web app, so a
broken production build went unnoticed and the server could not serve the
build at all. Service workers and web push also need the production web app
behind HTTPS, which the deploy notes did not cover.
deploy.sh now builds the web app after install into a staging directory and
swaps it into apps/web/dist right before the restart, so a failing build
stops the deploy before migrations and the restart and never touches the
build a running vite preview serves. Any failure after the checkout rolls
back the code, reinstalls, regenerates the client, rebuilds the web app when
this deploy had already replaced it, restarts and health-checks; the outcome
is recorded as rolled-back or rollback-failed. The health check now fails
when the API check fails even if the web check passes. Build output goes to a
log that is printed only on failure.
start-stack.sh runs api, worker and sandbox supervisor as pnpm dev does and
serves the web app either from the Vite dev server (default) or from the
build with vite preview on the same port (EVO_WEB_MODE=preview). Preview mode
first checks that a build exists and that RAKAZO_HOST and SCREEN_PROXY_SECRET
resolve the way vite.config.ts resolves them, using the same secret guard.
evoo-local.service is a user unit template for it; the README documents
installing it, switching modes, the catalog caveat for production builds,
and HTTPS through Caddy with EXTRA_TRUSTED_ORIGINS.
A testkit test runs the real scripts against fixture checkouts with fake
pnpm, curl and restart hooks. It replaces the unwired shell regression test,
which exercised a copy of the SHA capture rather than deploy.sh itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* ops: show the build error instead of stack frames when a deploy build fails
A failing web build printed the last 40 lines of its log, which for a syntax
error are mostly bundler stack frames below the message that names the file
and line. Drop the frames before printing the tail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test: look up the home main conversation in the scheduling journey
Since every project has its own main conversation, the journey that
schedules for a specialist found the user's main conversation with
findFirst over all of them. With the Lab project's conversation also
matching, it picked either one, so the check failed about one run in
three. The lookup now names the home space, where the specialist joined
and the routine is created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(ui): make the light theme a real light theme
The web shell hardcoded dark surfaces (panel hex, white/black alphas,
white text), so switching to light left the sidebar, chat, composer and
side panel dark with ink text on top. Shell surfaces now use the shared
tokens: light gets a warm paper frame with white panels, hairline borders
and short soft shadows; dark keeps its look via dark: variants.
- Light palette: visible borders (border was equal to muted), white
primary-foreground, lighter user bubble, readable warning/success.
- Selected sidebar row lives in one place (roster-row-style).
- Markdown, inline code, code blocks, quotes and scrollbars follow tokens.
- cn() now knows the text-ui-* scale is a font size; tailwind-merge
treated it as a color and dropped labels' color on resized buttons
(sign-up submit rendered ink on ink).
- Stage bloom was offset left by a double translateX.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016or6oqCqP8wbNafvhnbLQT
* refactor(ui-web): leave the cn() fix to #115#115 fixes the same tailwind-merge bug more completely (text, control,
icon and spacing scales, guarded against styles.css) and adds the same
two files, so this branch drops its narrower copy to avoid the overlap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016or6oqCqP8wbNafvhnbLQT
---------
Co-authored-by: Claude <noreply@anthropic.com>
* chore: remove legacy orchestrator, trading and one-off script leftovers
The repository still carried the pre-evOo prototype: a Python orchestrator,
the old HTML "evoo-platform" cockpit (whose test ran in the root unit
suite), an apex-trading package, the hot-reload deploy.sh for it, a
permissions manifest, an architecture note with local file links, agent
handoff notes, a Steve Jobs persona template and about ninety one-off
scripts that probe a developer database with hard-coded local URLs. None
of it is used by the product, CI or the real deploy in ops/evo-deploy.
Delete those files, drop the evoo-platform test glob from the root vitest
config, the Cockpit section from .env.example and the dead biome and
gitignore entries, and update the docs that listed the removed tools.
scripts/start-tmux.sh no longer starts the removed sentinel and resolves
the checkout from its own location instead of a machine-specific path;
scripts/seed-ollama.ts requires DATABASE_URL instead of falling back to a
local URL with a username.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* fix(web): drop trading and persona prompts from the conversation shell
The web shell still showed leftovers from the trading prototype: composer
chips with German trading and "Steve Jobs" prompts, trading suggestion
chips under the empty conversation line, a German trading hero for the
empty transcript, and a German "data protection" status panel in the
sidebar. They suggested actions the product does not have and added
persistent chrome.
Remove the composer chips and their helper, the empty-state chips and the
hero (the empty conversation keeps its existing one-line start hint), the
sidebar trading indicator and the data trust panel. The new-bot E2E now
asserts the plain empty state; the sidebar spec no longer expects the
trading status, and the placeholder Steve Jobs spec with no assertions is
gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
* refactor: remove the trading bot status integration
With the sidebar trading indicator gone, the only remaining consumer of
the trading bot status was a banner in General settings that reused the
same component. evOo is a personal assistant; a status proxy and poller
for one external trading service, configured through service-specific
TRADING_BOT_URL and VITE_TRADING_BOT_URL variables, does not belong in
the core product.
Remove the settings banner and its prop plumbing, the web poller, the
indicator component, the /api/trading-bot/status proxy, the shared core
contract, the env loading and both variables from .env.example, together
with their tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore: remove one-off database scripts and a stale review note
packages/db carried four hand-run scripts outside the package build that
query or seed a developer database with a hard-coded connection string,
one of them for a specific account. docs/VOICE-FIRST-REVIEW.md was a
one-time verification note for a workflow that no longer exists and
named private machines and local paths. Nothing references them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* chore: replace machine-specific paths and addresses with placeholders
ORIGIN.md pinned the intended checkout to an absolute path under a
personal home directory, docs/LOCAL-SETUP.md listed folders on one
developer's machine, and two tests used what looks like a real tailnet
address. Use a home-relative path, drop the machine-specific folder list,
and use 100.100.100.100 in the tests; it is still inside 100.64.0.0/10,
so both tests check the same thing as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
* test(web): drop the trading status from the shell contrast test
main's contrast test now waits for the sidebar trading status before
measuring; this branch removes that status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
Each project (space) now has a main agent and a main conversation of its
own. The user talks to it, and it delegates to the project's specialists.
Creating a project sets both up and opens the conversation, and deleting
an otherwise empty project removes them with it.
Claude-Session: https://claude.ai/code/session_01R8hSsKTB3W3e61wD6kvzur
Co-authored-by: Claude <noreply@anthropic.com>
Lazy-load the desktop-settings page and its heavy sections so
loadWithRecovery can retry or reload after ERR_NETWORK_CHANGED.
Show the no-desktop alert immediately when the bridge is absent.
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
* ci: soft-fail mobile OTA when EXPO_TOKEN is missing
OTA publish is an infra helper; missing Expo credentials should not
fail the ci workflow or block evo-deploy after green Product CI.
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
* test(updater): narrow optional workflow jobs and format the OTA assertion
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mFCmHk4zPyWpD9yY2C3yJ
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Every image publish on main failed at "Resolve registry attestation
eligibility" with "accepts 1 arg(s), received 2". The step ran
`gh api ... --jq -r '.owner.type'`, but gh api has no -r option: --jq
took "-r" as its filter and the real filter became a second positional
argument. gh api --jq already prints strings without quotes, so the
filter is now passed directly. The images were built and their
manifests pushed; only the attestation decision failed.
The workflow test now rejects `--jq -r`. Run with the repository's
values, the fixed step reports a personal account and a private
repository and skips attestation, as the guard intends.
Claude-Session: https://claude.ai/code/session_01EyQw3AEaHYXegvgvTPAsi1
Co-authored-by: Claude <noreply@anthropic.com>
The live screen opens wide beside the chat while the bot works on it or asks
for a takeover, a file the bot presents as its result opens next to the
conversation, and a question that scrolled away gets a "Needs you" pill back
to it. Shared core rules derive the focus from signals clients already have
(pointer events, takeover requests, a new `present` flag on attach_file,
pending asks), move the pane only on focus edges, never replace a pane the
user opened, and learn per bot whether the screen opens on its own. Pane
widths persist per device; mobile shows a pill that opens the computer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7VSRHHzyt2KwoCkUkZ2Gc