148 Commits

Author SHA1 Message Date
Claude
6d7d46024d
Merge image edit titles in the user's language
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 14:11:48 +00:00
Claude
31f4c2531d
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
2026-10-03 14:11:27 +00:00
Claude
4cda7f79f7
Merge video editing and export with ffmpeg
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 14:05:48 +00:00
Claude
0b3e0638ca
Merge media job fixes: GPU floor, settings test, data-sharing recipients
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 14:05:48 +00:00
Claude
97cede6c9c
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
2026-10-03 14:02:06 +00:00
Claude
9b4b9c8465
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
2026-10-03 14:01:14 +00:00
Claude
4e545c20f4
Merge the media integration branch into video editing again
Picks up ComfyUI generation, media tool dispatch and the streaming runner.
The video tools now share the image tools' schema helper, and both sets of
operations join the runtime's operation list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:57:18 +00:00
Claude
4cdc49e6d8
Merge claude/phase3-media (ComfyUI operations) into the media jobs runtime
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:53:47 +00:00
Claude
01eea83b05
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
2026-10-03 13:53:47 +00:00
Claude
f14d91d24a
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
2026-10-03 13:51:44 +00:00
Claude
533f82d856
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
2026-10-03 13:51:13 +00:00
Claude
faab1256c9
Merge media job RPCs, tool dispatch and the streaming runner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:50:39 +00:00
Claude
2796965256
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
2026-10-03 13:47:44 +00:00
Claude
3e046142d5
Merge ComfyUI image and video generation and image edits
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:47:44 +00:00
Claude
66a257b083
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
2026-10-03 13:46:34 +00:00
Claude
44702e3770
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
2026-10-03 13:45:50 +00:00
Claude
3b97805f20
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
2026-10-03 13:45:50 +00:00
Claude
4217e4b4af
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
2026-10-03 13:42:35 +00:00
Claude
e0277985f9
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
2026-10-03 13:37:19 +00:00
Claude
24c9245af1
Merge claude/phase3-media into the media jobs runtime
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:36:03 +00:00
Claude
fad25678ac
Merge the media integration branch into the ComfyUI workstream
Brings in artifact streaming and the media job runtime, settings and tool
helpers that the ComfyUI operations register with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:35:47 +00:00
Claude
d8c324a7af
Merge the media integration branch into video editing
Brings in artifact streaming and the media job runtime that the video
operations register with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:35:46 +00:00
Claude
485090c391
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
2026-10-03 13:35:34 +00:00
Claude
d54d8bff88
Merge the media job runtime into the media integration branch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:32:20 +00:00
Claude
1f33b48362
Merge claude/phase3-media (artifact streaming) into the media jobs runtime
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:32:17 +00:00
Claude
d49bec4f8d
Merge artifact streaming into the media integration branch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:31:58 +00:00
Claude
c60c693d13
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
2026-10-03 13:31:53 +00:00
Claude
62e9bc67f1
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
2026-10-03 13:31:39 +00:00
Claude
fa0986bd2d
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
2026-10-03 13:26:59 +00:00
Claude
7ff93f08bb
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
2026-10-03 13:04:16 +00:00
cynarAI
3d180dc2dd
fix(web): keep chat text readable in the light theme (#104)
Chat markdown hard-coded a near-white text color, so on light surfaces
(ask cards, delegated task briefs, peer and file previews) the text was
white on white. The desktop conversation panel and the bubble and glass
surfaces were hard-coded dark while the text on them follows the theme,
so in the light theme the user's own messages rendered as empty pills.

- Chat markdown uses the foreground token; heading, code and quote colors
  come from the shared @rakazo/chat-ui markdown styles instead of
  white/emerald overrides. Quotes read in the body color, as on mobile,
  so they stay legible in the dimmed transcript.
- User bubbles use the chat-user tokens; bubble and glass card surfaces use
  card and border tokens.
- The conversation panel uses the background token on desktop too, as it
  already did on mobile. Bot reply footers, summary cards, quick prompt
  chips, the awareness strip and the chart expand button drop hard-coded
  white/zinc/emerald classes that would vanish on that panel.
- New web E2E measures the contrast of the user's messages, a markdown bot
  reply, a delegated brief and an ask card in a group transcript in both
  themes and captures screenshots.


Claude-Session: https://claude.ai/code/session_01WnqLpAn8253EY1eCLKVJb2

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-03 14:43:05 +02:00
cynarAI
110984b7bc
ci: skip image attestation on user-owned private repos (#103)
GitHub artifact attestations are unavailable for private repositories
under personal accounts. The publish job already tags images successfully;
only the attest step failed. Gate attestation on visibility and owner type
so org and public repositories still attest.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-03 13:57:00 +02:00
cynarAI
653ea6abe5
ci: isolate digest artifacts on self-hosted runners (#102)
Build and publish used /tmp/digests, which persists on evoo-ci hosts.
Uploading /tmp/digests/* picked up stale digest markers from earlier runs,
so the manifest step saw 13 digests instead of one amd64 image.

Use RUNNER_TEMP for recording, uploading, and assembling digests so each
job only sees its own architecture marker.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-03 13:02:55 +02:00
cynarAI
a35e431d1c
feat: main agent with a main conversation, user model and delegation (#101)
* feat(db): main agent, main conversation, user model and delegated task foundation

The product needs one main agent that leads a main conversation, knows the
user across spaces, and hands work to visible specialists. Today none of
that exists in the data model: "Chief" is a client-side naming convention,
the "Hauptthread" is a group the web client creates by name, and nothing
stores what the agents know about the user.

This adds the shared foundation the Phase 1 features build on:

- Bot.kind ("main" | "specialist", one main bot per user) plus capability
  fields (tags, builtin tool allowlist, model tier).
- ChatGroup.kind ("group" | "main", one main conversation per user) and
  ChatGroup.leadBotId.
- UserProfile and UserFact with category, confidence, source, consent
  status and per-space visibility; embeddings are stored as float arrays
  because none of the Postgres images ship pgvector.
- DelegatedTask for work the main bot hands to a specialist.
- SpaceModelTier to map a model tier to a connected model per space.

The migration backfills each user's onboarding bot in the home space as the
main bot and turns an existing "Hauptthread" group containing it into the
main conversation. CHECK and foreign key constraints on existing tables are
added NOT VALID and validated in a separate migration, as elsewhere in this
repo.

New db helpers ensure the main bot and the main conversation (idempotent and
race-safe, repairing a lost lead), add same-space specialists to the main
conversation and announce it with a group.updated event, and load the user
context a run may see. The main conversation is exempt from the two-member
floor and cannot have its members replaced through a group update. The
contracts expose the new fields and the user model and delegated task
schemas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* feat(adapters): every run knows who it is and who the user is, in the user's time zone

Runs dropped the bot's name, title and description whenever custom
instructions existed, always stated the time in UTC, and knew the user only
through free-form durable memory. Groups with a lead, like the main
conversation, still got the peer-group lines that forbid handing work back.
And every group follow-up run failed during setup: the inbound handoff lookup
selected a `bot` relation that Message does not have, which Prisma rejects.

- Instructions now open with the bot's identity, then its own instructions,
  a main-agent role paragraph for the main bot, the current time in the
  user's time zone with its offset (UTC when unknown), and a `<user>` block
  built from loadUserContext: profile fields and the active facts visible in
  the run's space, escaped, clipped and framed as data rather than
  instructions. Runs that reply in a shared messaging channel get only the
  name, zone, locale, language and style, matching how those runs already
  leave out durable memory.
- run_subagent helpers receive the identity, time and user block through a
  new optional AgentRunRequest.subagentContext instead of nothing.
- Groups with an active lead get lead and specialist lines and matching
  inbound handoff instructions. Members may hand a stage back to the lead and
  the lead may delegate the next stage; the bounce rules still apply between
  members. Ordinary groups keep today's lines.
- The teammate directory is rendered only when the run has message_bot, which
  group runs never have, and tool guidance names a tool only when the run has
  it.
- The auto-review judge, history compaction and the team chat judge read the
  time in the user's zone, and schedule_create defaults to it.
- The inbound handoff sender is read from the handoff block and looked up
  among the user's bots; a Postgres test covers the follow-up run, the prompt
  it receives, and handing the result back to the lead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* feat(memory): learn the user profile from conversations; add built-in semantic memory

Why: the user profile only knew what onboarding wrote, so people had to repeat themselves,
and semantic recall needed a hosted or separately run memory service. The profile now grows
from everyday conversations, and recall can run on the user's own server with a local
embeddings endpoint (for example llama.cpp). Both stay optional and are tested offline.

What:
- adapter-kit: EmbeddingProvider contract (describe, embed) and the profile.consolidate job
  (payload schema and a replace-keyed job factory, one pending job per user).
- adapters: OpenAI-compatible embeddings (POST {baseUrl}/embeddings through the guarded
  OpenAI-compatible fetch, batching, bounded responses, clear errors), a deterministic
  emulator, an in-process /v1/embeddings server emulator, and a shared conformance suite.
- adapters: "builtin" semantic memory stored in Postgres (semantic_memory_items with Float[]
  embeddings, cosine ranking in process, bot/shared/history scopes matching the existing
  providers), registered in the memory provider factory and configured through
  memory.connectProvider with an embeddings URL, model and optional API key. Private
  endpoints require the deployment owner, like the other local memory providers.
- adapters: profile consolidation reads the user's settled app conversations since
  UserProfile.consolidatedAt (never messaging channels), asks the main bot's model for strict
  JSON facts and an updated summary, drops restatements (embedding cosine when built-in
  memory is configured, normalized text otherwise), stores sensitive categories as pending,
  and advances the cursor with a compare-and-set. A reconciler hook enqueues it at most daily
  per active user; it skips under the scripted runtime and without a usable model.
- db: additive migration 20261002210400_ws4_semantic_memory.
- web: a "Built-in" option in Memory settings; AI data sharing names the embeddings server.

Tests: new embedding conformance, OpenAI-compatible wire, built-in provider and consolidation
unit tests; new Postgres suites for built-in memory (including a real loopback embeddings
server through the memory provider resolver) and for consolidation and its scheduling query,
both added to the integration harness. The settings E2E spec now opens the new form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* feat(models): map model tiers per space and let bots and helpers ask for one

A space can now map the tiers fast, standard and code to connected models,
including several models served by one OpenAI-compatible router endpoint
(one credential, many model ids). A bot can ask for a tier, and a single
run_subagent call can ask for one. A tier with no model mapped falls back
to the usual model choice, so nothing breaks before tiers are set up.

- db: SpaceModelTier repo (list, find, set or clear); tier mappings are
  copied into a new space like model and voice preferences; createBot
  carries a tier, so duplicates keep it and spawned bots inherit it.
- contracts/api: models.tiers.list and models.tiers.set;
  models.probeOpenAiCompatible can list a connected server's models by
  credential id with its stored address and key; bots.update accepts
  modelTier and keeps a pinned model and a tier mutually exclusive, so a
  model picked on a surface that knows no tiers (mobile) is never silently
  overridden. Thinking-level validation follows the tier's model. AI
  consent status discloses tier models; desktop local settings allow the
  tier procedures.
- adapters: selectConfiguredModel runs a mapped tier ahead of the pinned
  model and the space default; the executor resolves bot tiers for runs
  and model lookups, and helper tiers through resolveModelTier.
  run_subagent takes model_tier; an explicit model wins over it.
- web: a collapsed Tiers section in model settings (shown once a model is
  connected) and mapped tiers as choices in the bot Model select.
- docs: tiers in self-host model setup.
- tests: resolution order and fallbacks (selection, executor, Pi helper),
  repo, API and contract units, authorization and offline Pi Postgres
  suites, and a model-tiers web E2E spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* fix(adapters): state the consolidation time in the user's zone; name the memory migration after its content

Profile consolidation now passes the profile's time zone to the shared time
instruction, which takes the zone since every run states the time in the
user's zone. The semantic memory migration is renamed after what it creates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* fix(adapters): keep the user's summary and facts out of replies to other owners' agents

An agent connection wakes a bot with another owner's message, and the reply
goes back to that owner. Such runs now get only the basics of the user block
(name, time zone, locale, language, style), like runs in shared messaging
channels, so confirmed facts such as health never reach another person's
agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* feat(profile): readable, writable and forgettable user model

The main agent is meant to know the user, but nothing could read or change
the user model the foundation added. Onboarding pasted the user's answers
into the first bot's instructions as a "User Context" block, and the user
had no way to see or remove what their bots knew about them.

This makes the user model a user-controlled resource:

- db: a profile repository scoped by user. One atomic update writes the
  account name, profile fields, new facts (deduplicated by normalized
  text, capped per user), fact edits including per-space visibility, and
  confirmations. Health and values facts wait for confirmation unless the
  user wrote them. Forgetting hard-deletes facts by id, category or all,
  clears the summary that may restate them and marks earlier chats as
  consolidated so they cannot teach them again; forgetting everything also
  clears the profile fields. Writes take a per-user advisory lock.
- api/contracts: profile.get, profile.update and profile.forget. Unknown
  fact ids are a 404 on update and ignored on forget; spaces outside the
  user's memberships are rejected.
- adapters: an update_user_profile tool for the main bot only, hidden from
  specialists and shared messaging channels and refused if called anyway.
  Facts carry source "conversation" and the bot, thread and message they
  came from. The scripted runtime records "learn about me: [category:]
  statement" for tests.
- web: onboarding writes the name, time zone (browser default, now
  prefilled and editable), locale, chosen language, communication style
  and the hobby and work-style answers to the profile instead of
  overwriting the bot's instructions, and the harvested memory section no
  longer repeats them. A new "About you" settings section lists profile
  fields and facts by category with their origin, confirms pending facts,
  and forgets one fact or everything.

Tests cover the contracts, the repository (unit and PostgreSQL), tool
selection and refusal, the scripted trigger, the onboarding mapping and
view helpers, an API suite that runs the tool through the executor with a
real main bot, authorization for the new procedures, and an E2E spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* feat(delegation): main bot hands work to specialists as tracked tasks

The main agent had no way to give a specialist a piece of work and keep
track of it. message_bot is a fire-and-forget peer message whose work stays
hidden from the user, and handoff_to_bot ends the main bot's turn and only
works between members of one group. Delegation now runs the specialist's
work where the user follows the main agent and brings the result back so
the main agent can close the task.

- delegate_task({bot, title, brief, deadline?}) is offered to the main bot
  only and refused unless the caller leads the main conversation. It
  resolves the specialist by id or name across the user's spaces, creates a
  DelegatedTask and posts a task card in the main conversation.
- A specialist from the main conversation's space joins it and works there
  in a run with the new trigger "delegation", which the peer filters do not
  hide, so its progress, asks and approvals appear in the main conversation
  with the specialist as sender. A specialist from another space works in
  its own chat; its written result is posted into the main conversation as
  its message.
- The executor mirrors the run on its task (running, waiting, done, failed,
  cancelled) after the run starts and when each attempt ends, and updates
  the card in place through thread.message.updated. Done and failed tasks
  wake the main bot in the main conversation with the result. The job
  reconciler settles tasks whose run ended without the executor (stop,
  clear, archive, deletion, crash).
- tasks.list({threadId?, status?}) returns the caller's tasks for the
  current space.
- The task card shows who, what and status with Lingui labels; the brief
  opens on demand. The message_bot card no longer shows the recipient's
  unrelated overall status or hard-coded German copy.
- The scripted runtime understands "delegate to the bot named X: ..." and
  the task wake prompt; activity rows name delegation runs by task title.

Tests: unit tests for the prompts, status mapping, card, reducer, tool
selection and scripted runtime; a Postgres suite for same-space, group,
other-space, ask and answer, failure, stop and replay; tasks.list in the
authorization suite; and an end-to-end spec for the whole flow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* fix(adapters): show the main agent the specialists it can delegate to

The main conversation is a group run, and group runs no longer list the
teammate directory, so the main agent could not see the names of the
specialists delegate_task expects. Runs that have delegate_task now list
the user's specialists across the spaces they belong to, those next to the
conversation first and others marked with their space, and that list
replaces the teammate directory so no bot appears twice. It also says when
to prefer delegate_task over handoff_to_bot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* feat(inbox): show what specialists need from the user in the main conversation

Specialists ask questions, request approvals and the computer screen in
their own threads, and the user only finds out by opening each thread. With
the main conversation as the single entry point, those requests have to
reach the user there, with the specialist as the visible sender.

- inbox.list returns, for the current space and the user's own bots, pending
  asks and approvals (bot messages with a pending ask block whose run waits
  for input), takeover requests, and unread specialist threads. Asks already
  in the main conversation are left out; takeovers stay because their
  controls live in the bot's own thread. Each item carries the bot, the
  thread target, run and message ids, and the ask block for asks and
  approvals.
- The main conversation shows the items above the composer. Asks and
  approvals are answered in place through threads.answer with the source
  thread target; takeovers and unread threads open the bot's thread. The
  list refreshes when the bots' status, unread state or newest message
  changes, every 15 seconds, and after an answer.
- The inbox shrinks and scrolls so it never pushes the composer out of view.
  The transcript now clips its scroller: with no room left in the column,
  the scroller's padding used to spill over whatever sat below it.
- The ask block schema is extracted from MessageBlock so the inbox contract
  reuses it, and AskCard accepts a className.

Tests: unit tests for the item builder and the component, a Postgres suite
for the query (added to the harness list), inbox cases in the authorization
suite, and an E2E spec in which a specialist asks in its own thread and the
user answers from the main conversation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* fix(adapters): write learned facts under the profile's lock and limit

Profile consolidation wrote facts beside the user profile repository: without
its per-user lock, its duplicate check at write time, or its limit of 500
facts. A long-running profile could grow past the limit and then refuse the
user's own additions. Consolidation now takes the same lock, drops facts the
profile gained since the run read it, and keeps only what still fits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* fix(adapters): keep profile writes out of replies to other owners' agents

A run woken over an agent connection hears another owner's bot, not the
user, so update_user_profile is withheld there: nothing such a conversation
says can become a fact about the user. Detection moves ahead of tool
selection; the reduced user block uses the same flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* feat(capabilities): least-privilege builtin tools per bot and schedules on behalf

Every bot saw every builtin tool, and nothing outside the runtime's own
declaration kept a run to the tools it was given: the executor ran any tool a
runtime asked for, and the Pi runtime exposed all builtins whenever a caller
passed an empty tool list, which the safety checker, the history summarizer
and the team chat judge do on purpose. A specialist could not be kept to the
tools of its lane, bots had no capability tags, and the main bot could only
schedule routines for itself.

- A bot can carry a builtin tool allowlist. Its runs see only the allowlisted
  builtins, and an empty allowlist means none. MAIN_ONLY_BUILTIN_TOOLS (empty
  until the user profile and delegation tools land) never reach other bots.
  applyTool refuses any tool the run did not expose, and an empty tool list
  now means no tools for the Pi runtime and its subagents.
- A bot spawned by a restricted bot inherits the restriction and a duplicate
  keeps it, so neither path widens a bot's tools.
- bots.create and bots.update accept tags, restrictBuiltinTools and
  allowedBuiltinTools; the router rejects names that are not builtin tools.
  bots.builtinTools lists the names a bot's allowlist can use.
- Bot settings get a Tags field and a switch with a tool checklist under
  Advanced.
- The main bot's schedule_create takes an optional bot: one of the user's
  bots in any space the user still belongs to. The routine belongs to that
  bot in its space and wakes in the main conversation when the bot is a
  member there, otherwise in its 1:1 thread. Routines record the bot that
  scheduled them (additive migration), so schedule_list and schedule_cancel
  also cover exactly the routines a bot scheduled for others. Cancelling by
  a name that matches several routines is refused.

Tested with unit tests for the per-bot filter, the applyTool guard, the empty
tool list, and on-behalf resolution, creation, listing and cancelling; with
Postgres journeys for on-behalf routines waking across spaces and in the main
conversation and for allowlist enforcement through the scripted runtime; and
with a web E2E for the settings section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* test(adapters): give the consolidation unit tests the profile lock

The write transaction now takes the per-user profile lock, so the mocked
client answers the lock query, and the first case checks the lock is taken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* feat(adapters): run profile consolidation on the fast tier when one is mapped

Fact extraction is short structured work, so consolidation asks the model
resolver for the fast tier. The resolver takes a per-call tier and, when the
space has not mapped it, keeps the bot's own tier or model as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* feat: land in the main conversation, led by the main bot

After onboarding the user ended up in a 1:1 chat with "Chief", and the
"Hauptthread" was a group the web client created and found by name. The
main bot had no conversation it actually led, and addressing another bot
there did nothing unless the user had added it as a member first.

Now the main conversation is a real place:

- bots.ensureMain creates or returns the main bot. Bootstrap and space
  navigation return `main` ({ botId, groupId, spaceId }) once the main
  bot exists; reading navigation never creates a bot.
- The main bot leads the main conversation and answers when nobody is
  addressed: message routing, follow-ups, reactions and clearing the
  thread fall back to the group's lead instead of its first member.
- Addressing a same-space bot that is not a member yet, by @Name or a
  mention chip, makes it join before the message is routed, so it
  answers in the conversation. Specialists from other spaces do not join.
- The main bot cannot be archived or deleted, the main conversation
  cannot be archived or deleted or have its members edited, and child
  bots never dissolve it. Renaming the main bot renames the conversation.
- Web: onboarding uses bots.ensureMain and finishes in the main
  conversation. The main conversation is the first sidebar entry in every
  space, shown with the main bot's avatar and name; from another space it
  switches back to the home space. The main bot is not listed again among
  the bots, its 1:1 thread stays reachable by URL. The client no longer
  creates or detects the "Hauptthread" by name, refetches a group when it
  changes, and shows the speaker of any of the user's bots in a group.
- Onboarding no longer resets the step when its bootstrap resolves after
  the user has already moved on, and new spaces open in the app instead
  of walking onboarding again.

Tests: group mention routing with a default responder, lead follow-ups,
child-bot guards, a journey for the main conversation (lead answers,
addressed bot joins, guards, rename), authorization for bots.ensureMain,
and an E2E spec that lands in the main conversation, gets an answer from
the lead, sees an addressed bot join, and reaches the conversation from
another space. Existing E2E specs start in the main bot's thread through
a helper or use the main conversation where that is what they test.

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>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 06:44:20 +02:00
cynarAI
0beed6ad98
feat(web): aurora voice stage with live word-by-word caption (#100)
Replace the full-bleed cyan/violet/emerald bloom field with one dark glass
panel: the Oo, a live caption whose new words fade in, and a compact control
row (phase label, transcript, interrupt, hang up). An iridescent aurora rises
from the panel's bottom edge, follows mic energy while listening, drifts while
thinking and blooms while speaking. Its spectrum is the shared bot palette
rotated so the active Oo's color sits in the middle, so no new hex values.

Voice entry buttons drop the hardcoded cyan/amber styling for neutral tokens
plus a thin aurora rim when voice is ready. Dead voice CSS is removed and the
voice e2e now captures the stage.


Claude-Session: https://claude.ai/code/session_012G3dUMVDCDL3PwQE4WTQxL

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-02 22:41:30 +02:00
cynarAI
4c619b25b9
fix(api): keep the onboarding scan off the host unless the owner opts in (#98)
* fix(api): keep the onboarding scan off the host unless the owner opts in

The first-run scan walked the home directory of the API process user for
every account that signed up, with no consent, and its status endpoint
accepted any scan id. Any account could read the repository paths and
remotes of the server owner.

The scan now stays simulated unless the deployment configures
ONBOARDING_SCAN_HOME and the requesting account is the deployment owner;
the previous HOME fallback is gone. Scan records remember who started
them and the status endpoint answers only that account.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* ci: give each runner on a shared host its own E2E ports

Two Linux runners live on the same host. Every Web E2E job first frees
"stale" listeners on the fixed ports 3110, 3111, 5180 and 4321 with
fuser -k, so a job starting on the second runner killed the Vite server
of the job already running on the first one. The victim died with
SIGKILL (exit 137), and its remaining tests failed on reloads that could
no longer reach a server. This is what made the Web E2E run on this PR
red, and it explains the recurring reload failures on main whenever two
runs overlapped.

The workflow now derives API_PORT, WEB_PORT and WWW_PORT from the runner
name (suffix -N means an offset of N times 100), and the port-freeing
script follows those settings instead of a hard-coded list. The harness,
Vite, the API and the marketing site already read these variables.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
(cherry picked from commit 2f05ab6a817e5198e2a740706976108b7b1edde1)

* test(testkit): cover the env-driven E2E ports instead of a fixed list

The ports test parsed a hard-coded array that no longer exists. It now
runs the script on a fake host whose fuser finds nothing, and checks the
default ports, the per-runner override, the rejection of a non-numeric
setting, and that the workflow picks the ports before freeing them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
(cherry picked from commit cd0aedafcdec1b24dea924d8924b3b29af29d598)

* ci: keep every runner off the default E2E ports

Branches that still free the fixed ports 3110, 3111, 5180 and 4321 keep
running on the shared host (main, the nightly run, other open PRs) until
they carry the per-runner ports. A job of ours on the runner without a
numeric suffix used exactly those defaults, so such a neighbour could
still kill it, and its own port freeing could kill the neighbour. The
unsuffixed runner now starts at offset 100, so no job from this workflow
touches the defaults in either direction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
(cherry picked from commit ddf5febdb3072425442ef375ca0be63fbe09e7f1)

* fix(web): recover lazy routes and overlays from a failed module fetch

When the browser aborts a module request while a page loads, the lazy
import rejects and the route throws, leaving a blank page. Chromium does
this whenever a network interface appears or disappears mid-request
(net::ERR_NETWORK_CHANGED), which happens on a laptop that changes
networks and on the CI host every time a container starts or stops. A
Web E2E run hit exactly this: two modules of the onboarding page failed
to load and the suite waited on an empty document.

Lazy routes and overlays now load through a helper that retries the
import once and, when the browser keeps rejecting it from its module
map, reloads the page a single time before surfacing the error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
(cherry picked from commit d5dacb12f14380071ef14ba8845d18c9d39e3fac)

* fix(web): reload once when the module graph is aborted while booting

The lazy-route recovery cannot help when the browser aborts a module of
the static import graph: nothing of the app runs, the root stays empty,
and the only trace is a console error. A Web E2E run hit this right
after a page reload, when four modules of the shell failed with
net::ERR_NETWORK_CHANGED and the shell never rendered.

An inline script in the HTML entry now listens for the error event of
the module script and reloads the page once, guarded by a session flag
that the entry module clears after a successful boot. The existing
preload recovery stays in place; it only covers Vite's chunk preloads of
production builds.

A new E2E spec aborts one module request during boot and one lazy route
request, and expects the sign-in page to render in both cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
(cherry picked from commit e5b7e036e1a60e7e270592c4b9382ee23ed45ccb)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-02 21:48:46 +02:00
cynarAI
cd29fad674
chore(repo): ignore backups, handoffs and deploy state; drop stale leftovers (#96)
Backup copies, agent handoff folders, personal exports and the deploy
runtime state had been committed next to the real sources. Ignore those
patterns so they cannot be tracked again, and remove the leftovers that
still were: an npm lockfile beside the pnpm one, two Vite config backups,
two one-off screenshot scripts with a private address, and the deploy
state file that every deploy rewrites.


Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-02 21:43:02 +02:00
cynarAI
9c9188e657
ci: publish amd64 images under a lowercase name and deploy only after green CI (#97)
* ci: publish amd64 images under a lowercase name and deploy only after green CI

Image publishing never succeeded: GHCR rejects the capitalized repository
slug, and the arm64 jobs failed on the macOS runners, which have no Docker
buildx. Resolve the namespace in lowercase and build amd64 only until an
arm64 runner with buildx exists; the manifest check expects one digest.

The Evo deploy ran on every push to main regardless of the result of
Product CI, so red commits went live. Trigger it from the completed ci
workflow instead and deploy the head it verified; manual dispatch stays.

The Playwright report publish failed every nightly run because the S3
settings are not configured. The script now skips the gallery with a
notice when they are missing instead of failing the job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* ci: give each runner on a shared host its own E2E ports

Two Linux runners live on the same host. Every Web E2E job first frees
"stale" listeners on the fixed ports 3110, 3111, 5180 and 4321 with
fuser -k, so a job starting on the second runner killed the Vite server
of the job already running on the first one. The victim died with
SIGKILL (exit 137), and its remaining tests failed on reloads that could
no longer reach a server. This is what made the Web E2E run on this PR
red, and it explains the recurring reload failures on main whenever two
runs overlapped.

The workflow now derives API_PORT, WEB_PORT and WWW_PORT from the runner
name (suffix -N means an offset of N times 100), and the port-freeing
script follows those settings instead of a hard-coded list. The harness,
Vite, the API and the marketing site already read these variables.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* test(testkit): cover the env-driven E2E ports instead of a fixed list

The ports test parsed a hard-coded array that no longer exists. It now
runs the script on a fake host whose fuser finds nothing, and checks the
default ports, the per-runner override, the rejection of a non-numeric
setting, and that the workflow picks the ports before freeing them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* test(updater): expect the amd64-only publish and the lowercase namespace

The workflow test still described the multi-arch publish. It now checks
that only amd64 builds natively, that arm64 is documented as paused, that
one digest feeds the manifest, and that every image name comes from the
lowercase namespace step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* ci: keep every runner off the default E2E ports

Branches that still free the fixed ports 3110, 3111, 5180 and 4321 keep
running on the shared host (main, the nightly run, other open PRs) until
they carry the per-runner ports. A job of ours on the runner without a
numeric suffix used exactly those defaults, so such a neighbour could
still kill it, and its own port freeing could kill the neighbour. The
unsuffixed runner now starts at offset 100, so no job from this workflow
touches the defaults in either direction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* fix(web): recover lazy routes and overlays from a failed module fetch

When the browser aborts a module request while a page loads, the lazy
import rejects and the route throws, leaving a blank page. Chromium does
this whenever a network interface appears or disappears mid-request
(net::ERR_NETWORK_CHANGED), which happens on a laptop that changes
networks and on the CI host every time a container starts or stops. A
Web E2E run hit exactly this: two modules of the onboarding page failed
to load and the suite waited on an empty document.

Lazy routes and overlays now load through a helper that retries the
import once and, when the browser keeps rejecting it from its module
map, reloads the page a single time before surfacing the error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
(cherry picked from commit d5dacb12f14380071ef14ba8845d18c9d39e3fac)

* fix(web): reload once when the module graph is aborted while booting

The lazy-route recovery cannot help when the browser aborts a module of
the static import graph: nothing of the app runs, the root stays empty,
and the only trace is a console error. A Web E2E run hit this right
after a page reload, when four modules of the shell failed with
net::ERR_NETWORK_CHANGED and the shell never rendered.

An inline script in the HTML entry now listens for the error event of
the module script and reloads the page once, guarded by a session flag
that the entry module clears after a successful boot. The existing
preload recovery stays in place; it only covers Vite's chunk preloads of
production builds.

A new E2E spec aborts one module request during boot and one lazy route
request, and expects the sign-in page to render in both cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
(cherry picked from commit e5b7e036e1a60e7e270592c4b9382ee23ed45ccb)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-02 20:54:51 +02:00
cynarAI
4995b66b93
fix(e2e): wait for shell-ready before reaction assertions after reload (#95)
* fix(e2e): wait for shell-ready before reaction assertions after reload

Bootstrap can still be in flight after domcontentloaded on reload; match
#94/#87 by waiting on data-ready and the composer before asserting
persisted message reactions.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): shell-ready waits on transport reload and /app redirect

Apply waitForChatComposer after messaging-transport reload and allow
more time for /app to redirect before asserting the composer.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* ci: retrigger Product CI after team-computer flake

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* ci: retrigger Web E2E after runner OOM (exit 137)

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
(cherry picked from commit 6248b6c0c9b2dd07563dbac981bd43032d324989)
2026-10-02 17:18:45 +00:00
cynarAI
d1f7034aa5 fix(e2e): wait for shell-ready after group chat reload (#94)
* fix(e2e): wait for shell-ready after group chat reload

After voice/connect and full page reload on a group route, bootstrap
can still be in flight. Use waitForChatComposer (data-ready + Message
combobox) instead of asserting the composer immediately, matching #87.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): wait for shell before approval transcript after reload

Transcript messages are not painted until bootstrap finishes; match
group-chats and #87 by waiting on data-ready before asserting durable
approval copy survives reload.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* style: format approval-resume e2e import

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): wait for archived sidebar after space delete

After deleting a space the Archived section can appear only once
bootstrap finishes; wait for shell-ready after archive reload and assert
the Archived toggle before clicking.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): expect archived sidebar when last bot is archived

After archiving the only bot, the shell is ready without a Message
composer. Wait for data-ready and the Archived toggle instead of
waitForChatComposer on reload.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): wait for shell in composer-paste openComposer helper

Third paste spec failed when Message appeared before bootstrap finished;
reuse waitForChatComposer after navigating to /app.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-02 15:50:00 +02:00
cynarAI
a974f09520 fix(e2e): restore computer-update progress chip after reload (#93)
* fix(e2e): restore computer-update progress chip after reload

Harden the computer maintenance Playwright flow so mocked update state
survives a full page reload: stable background-chip test id, explicit
aria-label, wait for shell readiness and the updates RPC, and explicit
JSON route fulfillment.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(ci): bump Expo SDK 57 patches and harden flaky web e2e

Sync mobile Expo packages for frozen-lockfile typecheck and read RPC
fetch metadata before response bodies in Playwright route helpers.
Stabilize empty-project Starten, onboarding model catalog, and secret-card reload specs.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): stabilize RPC intercepts and project Starten flow

Fulfill patched RPC routes with string bodies, ignore benign route races,
target project Starten by test id, and wait for thread hydration after reload.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): drop threads/get wait after secret-card reload

Reload does not always refetch threads/get; poll the saved ask card instead.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): reserve unreachable setup port per desktop smoke run

Avoid CI races where a port reserved in beforeAll is rebound before the
unreachable-address check runs.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* ci: retry product checks after runner OOM on web e2e

Previous Web E2E job exited 137 during the Playwright suite; no failing spec was reported.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): wait for shell before opening user settings

After reload, the user menu is not interactive until bootstrap finishes.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-02 12:09:05 +02:00
cynarAI
23084b460b fix(web): + Starten for empty projects instead of fallback bot (#92)
* fix(web): show Starten for empty projects instead of fallback bot

When a project space had no group conversations, the sidebar fell back
to listing 1:1 bots (e.g. Chief from onboarding) under the project name.
Bootstrap also redirected /app to the first bot, making it look like an
existing conversation.

Only show project group chats in Projekte, render + Starten for empty
projects, and open the existing create flow (group or bot) on click.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(web): keep empty-project Starten view and route flows correctly

Rebased onto main. Guard the Hauptthread auto-redirect when the current
space is an empty project so the main Starten CTA stays visible. Split
project start into create-bot vs create-group flows (main vs sidebar) and
feed home-space bots into the group form when starting from a project.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(web): surface empty-project Starten on Projekte pillar

Route bare /app in empty projects to the Projekte view, keep /app on
stale bot/group URLs, and show the create-group side panel without an
active thread.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): scope RPC to selected space and tighten Starten specs

Pass x-rakazo-space-id from Playwright RPC helpers so archive/list hit
the active project space. Trim the group-create Starten test to the side
panel flow and navigate to /app after archiving the last bot.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* style(e2e): format empty-project-starten spec

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): remove project bots so empty spaces stay deletable

Archived bots still count as space content and hide the delete-space
menu. Use bots/remove in spaces.spec when the test needs a deletable
empty project space.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-25 20:54:06 +02:00
cynarAI
bc5f41ecb1 fix(adapters): route evOo self-modification requests to code tools (#91)
* fix(adapters): route evOo self-modification requests to code tools

Oos lacked product self-knowledge in their system instructions and were
steered toward browser/computer tools for UI text changes. Add explicit
guidance that evOo changes belong in source code (file/shell tools,
project/ when linked, cloud agent when configured), never via claude.ai
login or browser CSS injection.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(adapters): avoid space query that broke executor test mocks

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-25 18:42:45 +02:00
cynarAI
57521e3c80 Sidebar: rename 1:1 conversations section heading (#90)
* Sidebar: rename 1:1 section heading and add list spacing

Rename the sidebar conversations section from 'Unterhaltungen' to
'Sprich mit deinen Oo's', remove the '1:1 mit deinen Oos' subtitle,
and add modest vertical spacing above and below the conversation list.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* chore(mobile): bump Expo SDK 57 patch deps for typecheck gate

Expo published SDK 57 patch releases after main last passed CI; expo install
--check now fails on the pinned versions. Align mobile deps with the current
SDK expectations so the required Typecheck job stays green.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* chore(mobile): sync lockfile specifiers after Expo pin normalization

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-25 16:50:09 +02:00
cynarAI
fc173918f7 fix(e2e): harden onboarding-model-labels rpc/me intercept (#89)
* fix(e2e): harden onboarding-model-labels rpc/me intercept

Avoid passing a consumed fetch Response into route.fulfill alongside
json, which intermittently throws Response has been disposed in CI.

Add routePatchRpc helper that reads the body once and fulfills with
status, headers, and patched json only.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(e2e): migrate rpc/me intercept anti-pattern across specs

Extend helpers with fulfillFetchedRpc for multi-procedure or in-place
patches. Move onboarding, mobile, host-computer, and group-chats specs
off route.fulfill({ response, json }) after response.json().

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* style(e2e): biome-format host-computer-prompt rpc/me cast

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-24 08:16:39 +02:00
cynarAI
8bf343f0f8 fix(ops): keep git reset output off deploy SHA capture (#88)
git reset --hard prints 'HEAD is now at …' to stdout, which polluted
read -r previous_sha target_sha < <(git_fetch_and_checkout) and wrote
invalid values into .evo-deploy/state.json. Redirect git stdout to
stderr inside git_fetch_and_checkout and rollback; validate SHAs before
write_state and rollback.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-24 07:36:24 +02:00
cynarAI
5426bb73c5 fix(e2e): wait for shell-ready after RPC bot create (#87)
* fix(e2e): wait for shell-ready after RPC bot create

createNamedBot now waits for data-ready and the Message composer after
navigating to a freshly created bot, instead of asserting immediately on
a cold page load where bootstrap had not finished.

Also fix onboarding scan advancing to bot creation (onComplete was a no-op)
and use a fast simulated scan when AGENT_RUNTIME=scripted so CI E2E does
not walk the real filesystem during onboarding.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* style: apply biome format to touched onboarding files

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* style: clear biome lint in onboarding scan files touched by the fix

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* style: format onboarding-scan after rebase onto main

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* style: fix biome lint on main files blocking Product CI

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(onboarding): allow scan before first bot exists

The scan RPC required a non-empty botId, but onboarding runs scan before
Chief is created and the UI sent an empty string. That rejected the RPC,
left the flow on /onboarding, and every completeOnboarding helper timed out.

Make botId optional, omit it from the client when unknown, advance to bot
creation when scan completes, and keep scripted/fake runs on the fast scan path.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-24 03:38:40 +02:00
c18a04ce3f feat(api): sub-agent roles + DB project creation in onboarding scan
- Auto-generate specialist roles from detected interests/skills:
  Code Engineer, UI/UX Designer, Data Analyst, DevOps, Music, Writer
- Always include General Assistant role
- Embed role descriptions + instructions in agent briefing
- Create real Prisma Space entries for discovered git repos
- Deduplicate: skip spaces that already exist
- Pass prisma client through scan pipeline chain

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 00:17:29 +00:00
85a0f3860a feat(api): real onboarding scan — filesystem, Git repos, profile compilation
- Replace simulated scan with real filesystem scan of home directory
- Discover Git repositories via .git directory scanning
- Analyze repos: remote URLs, branches, last commits, language stats
- Compile user profile from file types, languages, and repo data
- Generate agent briefing with tech stack, interests, and skills
- Create project folders from discovered Git repositories
- 1500+ line implementation with comprehensive file classification
2026-09-23 23:43:51 +00:00
20afcb0d0b feat: security headers + onboarding scan & profile creation flow
Security:
- Add CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
  Permissions-Policy as Hono middleware (after CORS)
- CORS already hardened via isTrustedOrigin (localhost, private, Tailscale)

Onboarding scan flow:
- New 'scan' step between 'integrations' and 'bot' in onboarding
- OnboardingScanProgress.tsx: progress UI with 4 phases (scanning, connecting,
  building, briefing) with real-time polling
- onboarding-scan.ts: backend scan pipeline (in-memory registry, async phases)
- RPC routes: onboarding.scan + onboarding.scanStatus
- Contracts updated with new procedure definitions

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 23:31:12 +00:00