The privacy policy has always promised deletion on request, with emailing a
person as the only route. This is that promise, self-serve.
The dialog spells out what is lost before it asks, because the consequences
reach past the person clicking: every workspace they own goes, and anyone
sharing one loses that work. Confirmation is typing DELETE, the same bar the
destructive actions elsewhere use.
On success it reloads the page rather than routing, so no cached query
outlives the account it belonged to.
The startup bootstrap builds the current model shape with create_all, and the
claims table is part of that shape now. On any database where the backend
booted before this migration ran, the table already exists and the bare DDL
aborts the upgrade on "relation already exists".
With no transfer to offer, refusing the deletion would strand the user: the
409 told them to hand a workspace over using a feature that no longer exists.
So the preflight, the refusal, and the re-check in the task all go, and the
erase now deletes every workspace the account owns, shared or not.
Members of a shared workspace lose it. That is a real consequence for people
who did not click the button, so the dialog states it before the account is
erased rather than leaving them to discover it. A workspace the user merely
belongs to is untouched -- only their membership goes with them.
The tests move with the behaviour: the shared workspace is destroyed and its
colleague's own account survives, and owning one no longer holds the route
back.
It was built to keep a shared workspace alive past its owner, so that
deletion could hand it over instead of destroying it. Nobody has asked for
that. It costs a transfer endpoint, a preflight, and a per-workspace
resolution step in the delete dialog, all to serve a case we have never seen.
Deleting what you own is what every other resource here already does. The
transfer comes back the day a user needs their team's workspace to outlive
them.
The three that would be silent disasters: a shared workspace refuses the
delete, the erase leaves workspaces owned by other people alone, and a
workspace that gained a member stops the erase instead of cascading away.
Deactivating is the whole lockout: get_auth_context already turns away
inactive users on both the session and the token path, so no new column and
no auth changes. Refresh tokens are revoked and the cookie cleared, then the
erase is queued.
The blocker check runs server-side here as well as in the client, because
only the server's answer is a guarantee that nobody else's workspace
disappears. A failed enqueue puts the account back, rather than locking
someone out of a deletion that never runs.
Session-only: a leaked personal access token must not be able to destroy the
account it was issued from.
Owned workspaces go through the existing workspace deletion rather than the
FK cascade: the cascade drops the rows but leaves their blobs and knowledge
stores on disk forever.
The blocker check runs again here. A workspace that gained a member since the
request aborts the erase rather than being skipped -- "user" cascades to
workspaces, so deleting the row anyway would take a colleague's workspace with
it. The account stays locked out with its data intact, which is recoverable.
Stripe keeps its charges and invoices, which GDPR 17(3)(b) exempts as a tax
record; only the customer, with its saved card and contact details, goes.
Owned workspaces with other members in them would take that work down with
the account. Each carries its other members so the client can offer a
handover without a call per workspace.
Every multi-member case needs a second account joined to a workspace, and the
deletion suite is about to need the same two helpers the transfer suite grew.
All three ownership fields move together, a non-owner member cannot give the
workspace away, and it cannot be handed to a stranger -- that would need an
invite, which is its own flow.
Deleting an account must not take a shared workspace's other members down
with it, so the owner needs a way out that is not "delete everything".
Ownership is recorded in three places -- workspaces.user_id, the membership's
is_owner flag, and its role -- and any one left behind leaves the workspace
with two owners or none. The transaction stays open so the deletion flow can
hand over several workspaces and erase the account as one unit.
The former owner steps down to Editor rather than being removed: they may
still have work in there, and demotion is reversible by the new owner.
The behaviour the whole feature exists for: a fresh identity is credited, and
the same Google subject arriving on a brand-new user row is credited nothing.
A column default mints $5 for every row, which is precisely what makes
delete-and-re-register profitable. The grant moves into on_after_register,
inside the workspace transaction, so a rollback cannot leave an identity
marked as having taken a credit it never received.
The registration response reads the in-memory user, so the balance is
refreshed there too.
Claiming is a single INSERT ... ON CONFLICT DO NOTHING over every identity the
user presents, so two concurrent signups on one identity cannot both win. An
account whose identities are all already spent is awarded nothing.
A user no source recognises is ungated and says so in the log: that is
AUTH_TYPE=LOCAL, where the operator funds their own inference and there is
nothing to farm.
An empty ledger would hand every current user a free second grant the moment
the delete button ships. Backfill folds `people/<sub>` onto the bare subject
so a pre-169 row cannot claim twice, and skips deployments with no
oauth_account table at all.
Claim dates are the migration's own timestamp: "user" has no created_at to
copy from, and one shared value marks exactly which claims were inferred
rather than observed.
signup_credit_claims deliberately carries no foreign key to "user": the row
whose whole purpose is to remember a spent credit must survive the erasure of
the account that spent it. Unique on (kind, fingerprint) so the grant is a
single insert that either wins or loses.
`sub` is stable per Google account and survives the user renaming their
mailbox, so it identifies the person without any per-provider email
normalization. Rows predating the `sub` switch stored `people/<sub>`, which
migration 169 deliberately left in place, so the prefix is stripped before
fingerprinting.
Production is Google-only today, but password and other providers each need
their own way of naming the person. A decorator keeps adding one to a single
new file, with no edit to the granting path.
The ledger has to outlive the account that wrote it, so it cannot store a
subject id or an email. HMAC under SECRET_KEY rather than a bare hash: the
space of Google subject ids and email addresses is small enough to exhaust
offline against a plain SHA-256.
Three requirements that pull against each other: users must be able to erase
themselves, erasing must not re-mint the $5 welcome credit, and it must not
take a shared workspace's other members down with it. Records the identity
ledger, the Google `sub`-only v1 scope, and the rejected alternatives.
Added a new hook, useReasoningAutoScroll, to manage scroll behavior in the ReasoningEpisode component. This allows for automatic scrolling when new content is added, enhancing user experience during reasoning sessions. Updated the ReasoningEpisode component to utilize this hook, improving accessibility and interaction with the nested scroll area.
Uncomment the DAYTONA_* env passthrough on the backend service and add
DAYTONA_SNAPSHOT_ID, so a deployment that sets SANDBOX_PROVIDER=daytona
actually forwards the key, URL, target, and snapshot into the container.
Document the snapshot name in the compose .env.example.
The Daytona snapshot boots with `sleep infinity`, so bare `python3` is the
Ubuntu system interpreter, which lacks the office wheels (weasyprint,
reportlab, xlsxwriter, python-pptx, python-docx). OpenSandbox instead keeps
the image's code-interpreter entrypoint, which activates the 3.12 venv, so
its `python3` already imports them.
Result: office generation on Daytona silently failed for every format that
has no system-binary fallback — xlsx and pptx never wrote a file, surfacing
downstream as a FileNotFoundError during verification. Only pdf slipped
through, via LibreOffice conversion rather than the intended Python path.
Source code-interpreter-env.sh before invoking python in both sandbox entry
points (the deliverables `execute` tool and the Daytona `execute` heredoc).
On OpenSandbox this re-activates the same interpreter, so it is a harmless
no-op there.
Daytona's daemon returns a FILE_NOT_FOUND body as a plain DaytonaError
(sometimes typed DaytonaNotFoundError) instead of raising FileNotFoundError.
read_receipt only catches FileNotFoundError, so a missing receipt on the first
verification crashed the stream. Translate not-found failures to
FileNotFoundError to match the SandboxSession contract OpenSandbox already
fulfills; re-raise all other DaytonaErrors unchanged.
- Added new CSS animations for orbit, pulse, and breathing effects in the timeline activity indicator.
- Updated the `TimelineActivityIndicator` component to incorporate new animation styles and properties for improved visual feedback.
- Introduced additional CSS variables for pulse and breathe animations, enhancing the dynamic presentation of activity indicators.
- Refactored dot styles to include new animation parameters, ensuring a cohesive and engaging user experience.
- Added `load_source_document` tool to facilitate conversion of existing workspace files into PDFs.
- Updated document components to support download actions, including new `onDownload` props in `DocumentNode`, `FolderTreeView`, and `DocumentRightPanel`.
- Implemented download logic in `downloadFile` function to handle file retrieval from the backend.
- Enhanced tests for the new download functionality and source document loading.
- Introduced a new `getActivityPresentation` function to encapsulate logic for determining activity status and title, replacing the previous `effectiveActivityStatus` function.
- Updated `ActivityRow` and `TurnSegment` components to utilize the new presentation function, improving clarity and reducing redundancy.
- Renamed the `details` field to `progressTitle` in `ActivityData` to better reflect its purpose, and adjusted related parsing logic accordingly.
- Enhanced unit tests to validate the new activity presentation logic and ensure proper handling of progress titles.
- Changed the comment in ProgressReporter to clarify the role of coarse events.
- Renamed the 'details' field to 'progressTitle' in ActivityData and updated related methods to reflect this change.
- Adjusted tests to verify the new naming convention for progress tracking in activities.
- Removed data-active attribute from the `TimelineActivityIndicator` component to streamline its usage.
- Updated CSS styles in `globals.css` to apply animations directly to the activity indicator without relying on the active state.
- Enhanced `TurnHeaderContent` to manage the visibility of the activity indicator based on the showIndicator prop, improving user experience in the chat timeline.
- Added a new `TimelineActivityIndicator` component to enhance visual feedback in the chat interface.
- Updated styles in `globals.css` to support the new timeline animations.
- Removed the deprecated `PixelGridLoader` component to streamline the codebase and improve performance.
- Integrated the `TimelineActivityIndicator` into the chat message timeline for better user experience.
- Introduced a new `MentionIcon` component to centralize icon rendering for different mention types (folder, thread, connector, artifact).
- Updated `UserMessage`, `DocumentMentionPicker`, and `InlineMentionEditor` components to utilize the new `MentionIcon`, improving code clarity and reducing redundancy.
- Removed unused icon imports and consolidated icon logic for better maintainability.
- Enhanced the document mention picker to support dynamic artifact format display.
- Introduced new schemas for document viewing, including `DocumentViewFileRead` and `DocumentViewManifestRead`.
- Implemented a new API endpoint to retrieve document view manifests, providing metadata for document presentation.
- Added a document viewer component to handle various document types and their respective viewing logic.
- Updated the right panel to support document viewing alongside existing features.
- Enhanced tests to cover new document viewing functionalities and ensure robust error handling.
- Added `SANDBOX_OPERATION_TIMEOUT_SECONDS` to configuration for controlling sandbox operation timeouts.
- Updated various components to utilize the new timeout setting, ensuring consistent timeout behavior across different sandbox providers.
- Refactored execution logic in the sandbox tools to improve error handling and cleanup processes.
- Enhanced tests to validate the new timeout functionality and ensure proper execution flow.
- Added support for loading current deliverables and context for artifact revisions with `load_artifact_for_revision`.
- Removed the deprecated `load_artifact_source` tool.
- Enhanced documentation for artifact skills, including detailed revision instructions for DOCX, PDF, PPTX, and XLSX formats.
- Updated the Dockerfile to include the `python-docx` package for document generation.
- Adjusted artifact file roles in the database schema to exclude the source role.