* 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>
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
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
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
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
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
Main now gives every project its own main agent. Coaching stays with the
user's personal agent, the main bot of the home space: only it gets the
goal, habit, check-in, brief and nudge tools, the coach summary and the
open-onboarding note, and the tools refuse a project's main bot too. The
onboarding questions start with the personal agent only. A project the
scan creates gets its main agent like any other, and the inbox lists asks
from another space's main conversation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
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>
The onboarding scan keeps the consent-first rewrite. Since main's folder
check moved out of wireProjectWorkspace, applying a scan now checks that
each chosen project is still a folder (not only inside the scan folder)
before anything is written; a discovery test covers a project replaced by
a file after the scan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
spaces.create created the space first and only then checked the folder,
so a folder that was missing or not a folder returned 400 but left the
new space behind. The New project dialog shows that error, so fixing the
path and submitting again created a second project with the same name.
The folder is now resolved and checked right after the deployment-owner
check, before the space exists, and the space stores that resolved path.
wireProjectWorkspace takes the resolved folder instead of resolving it
again after the space was created. The onboarding scan, its other
caller, resolves and checks each repository folder the same way before
creating the project, and skips a folder that cannot be linked.
The authorization test has the deployment owner submit a missing folder
and a file (both 400) and then the real folder under the same name, and
checks that exactly one project with that name exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HG4pFTUBDpuCS2Y44TV8x
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
The consent-first scan replaces the automatic project creation that main
fixed in #116; main's git checkout fix and sidebar classes are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
spaces.create linked any workspacePath to the new project, and
computer.files and computer.readFile then listed and read files under
that folder for anyone in the space. On a deployment with signups, any
member could link / or the API's own checkout and read whatever the API
process can read, such as its .env file. Linking also ran git in the
chosen folder and stored its remote URL.
A linked folder lives on the host, and the host is the deployment
owner's machine, the rule the onboarding scan already follows:
- spaces.create refuses a folder from anyone but the deployment owner,
before the space is created or the folder is touched.
- Project files are listed and read only for the deployment owner, so a
space a member linked before this change stays closed too. Space
navigation no longer shows a linked folder's path and git remote to
other members.
- A linked folder is stored as its real path, and listing and reading
follow symlinks only while they stay inside it. Symlinks that leave
the folder or lead nowhere are left out of a listing instead of
failing it.
- The web New project dialog offers the folder field only to the
deployment owner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EyQw3AEaHYXegvgvTPAsi1
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
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
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
Brings in profile editing, the cross-space inbox and consent-first discovery. The wizard
keeps both changes: no preferences step, and the new scan step without the main bot lookup.
The harness lists both new onboarding Postgres suites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
Brings in ffmpeg (probing, extractAudio) and the video operations. The
transcribe registration now gets the ffmpeg audio tools, so video is
transcribed through extractAudio with the demuxer forced from the MIME type,
and long recordings are converted and uploaded in ranges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
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
- 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
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
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
Brings in the media job RPCs, generic media tool dispatch and the image
operations; transcribe joins the operation list and the tool catalog next to
them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
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
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
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
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
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
Brings in streamed artifacts with artifactUrl (replacing the local stand-in)
and the media runtime, settings and operation registry the transcribe
operation plugs into.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
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
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
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