Wework Transcript and Preference Cloud Sync
The Wework Core DSH plugin @wegent/dsh-transcript-sync synchronizes native
Codex rollouts, task workspaces, summaries using the previous schema, and
portable preferences. Cross-device restore no longer reduces a session to
user/assistant text or reconstructs history through thread/inject_items.
Availability and explicit opt-in
Cross-device synchronization is experimental and disabled by default. A user must first enable Experimental features under General settings, then explicitly enable Synchronize conversations and settings across devices under Cloud connection. While disabled, Wework keeps local tasks, conversations, and pending upload locators, but does not upload, download, or synchronize preferences.
When upgrading to the explicit-consent version, every existing device is reset to disabled once even if synchronization was previously enabled. The migrated state is persisted before any cloud synchronization request can run. If the user opts in again, the new consent version is recorded and later launches preserve that choice instead of resetting it again.
Storage boundary
The Backend uses three tables:
| Table | Purpose |
|---|---|
wework_transcripts | Transcript identity, branch relation, sequence, state, and writer lease |
wework_transcript_archives | Immutable native segment sequence, object key, SHA-256, size, and format |
wework_transcript_turns | One structured finalized-turn summary using the previous data contract |
The Executor encrypts bodies with AES-256-GCM before uploading them through the
authenticated Backend API. The Backend writes ciphertext to the private
wework-transcripts object bucket; desktop clients never receive object-store
endpoints, credentials, or presigned URLs. The wework_transcript_turns.payload
JSON retains the previous protocol's user messages, final assistant text,
reasoning summary, completion state, and task ID, but not the complete tool
protocol, usage, rollout JSONL, or workspace files. Each turn has its own row
instead of appending an entire transcript into one field; segmented tgz objects
carry full-fidelity capacity and exact restore.
The archive index, turn summary, and transcript head for one sequence commit in one MySQL transaction. A retry is idempotent only when both object metadata and summary match exactly. A missing or conflicting side is rejected instead of leaving a state that claims synchronization while either the tgz or summary is absent.
The Backend derives one stable key per user from
WEWORK_TRANSCRIPT_ENCRYPTION_SECRET and the user ID and returns it only
through the authenticated GET /{id}/encryption-key endpoint. All transcripts
owned by one user share that key, while different users receive different keys.
The key is never persisted in sync state, the outbox, or object contents. Each
segment nonce is derived from the key, AAD, and plaintext digest.
AAD binds the transcript ID, sequence, and format. Identical retries therefore
produce identical ciphertext for SHA-256 reconciliation without reusing a
nonce for different plaintext.
Each cloud sequence maps to exactly one object:
- Sequence 1, every tenth sequence, sequence 1 of a conflict branch, and the
first continuation after a cross-device restore are full encrypted
codex-snapshot.v1.tgz.aes256gcmsnapshots. Restore rewrites the local thread ID and workspace path, so a new snapshot establishes a portable byte baseline. - Other sequences are encrypted
codex-delta.v1.tgz.aes256gcmincrements. - Every segment also carries a workspace overlay so recent files are not lost.
- Workspace packaging excludes
.git,node_modules, build outputs, and common cache directories so repository objects and unrelated derived files are not uploaded repeatedly. - The outbox stores only task, session, turn, sequence, and branch locators.
- Native object snapshot pruning does not remove the corresponding structured summaries.
- After a new full snapshot is committed, the Backend retains the previous full snapshot and every later segment, then deletes older object bodies and metadata. With a snapshot interval of 10, an active transcript normally keeps 11 objects and peaks at about 20 instead of growing without bound.
- A native rollout segment has a 256 MiB plaintext limit. When building a delta, the Executor reads from the synchronized rollout offset instead of loading the entire growing rollout file first. A rollout that exceeds the previous 128 MiB threshold can therefore keep uploading while the pending segment remains within the limit.
Two computers may keep Wework open at the same time. Wework persists one stable
device ID for each desktop installation. While a lease is active,
writer_client_id identifies the current writer; after release, it remains as
the most recent writer, while writer_lease_expires_at alone determines whether
the lease is active. Clients poll only unarchived cloud progress every five
seconds. The most recent writer uploads that transcript without restoring it,
while another device automatically restores an unarchived transcript that is
missing locally. A fresh device therefore restores all unarchived tasks from
other devices without making the synchronization source repeatedly download
its own workspaces. Archived tasks are not restored automatically. Clients
acquire a short writer lease only while uploading, then release it immediately.
An idle office computer does not hold the lease, and a running or already bound
local task is never overwritten by restore. If both computers complete the same
sequence concurrently, the first commit remains on the main line and the second
becomes a deterministic branch, preserving both results. After a device creates
a conflict fork, its original local task moves to that fork, so synchronization
restores only the now-missing parent main line as a second local task. The fork
itself remains upload-only on its source device.
The existing wework_transcript_turns table remains in place with the previous
summary fields. Restore ignores this table, and it cannot replace the native
tgz.
State transitions
Conflicting rollout files are never merged. The cloud mainline remains
unchanged. The conflicting local chain moves to a branch derived from
clientId + transcriptId + turnId and uploads a full snapshot as branch
sequence 1.
Two-device verification
The GitHub CI transcript-sync desktop checkpoint starts real Electron,
Executor, and Codex processes and sequentially simulates device A and device B
inside one test. The devices use separate HOME, WEGENT_EXECUTOR_HOME,
WEGENT_CODEX_HOME, CODEX_SQLITE_HOME, Electron user data, application
configuration directories, and device identities. Switching to device B does
not delete or reuse device A's state.
The checkpoint must verify that device A uploads an encrypted snapshot and delta, device B restores the workspace and complete history from an empty state, and device B continues the conversation and uploads the next sequence. Every sequence must also create its structured summary. A restart test that shares local state is not an equivalent verification. Testing on two physical computers remains a release acceptance check for real network, sleep, and operating-system differences, but is not a prerequisite for GitHub CI.
Restore order
- Select the latest full snapshot at or before the current head.
- Download that snapshot and every contiguous later delta, verifying ciphertext SHA-256, format, and sequence.
- Authenticate and decrypt each object with the current user's key, verify its identity, restore the workspace in staging, and concatenate and parse rollout JSONL.
- Rewrite the workspace path and allocate a new thread ID on collision.
- Restore Codex
threadsandthread_dynamic_toolsstate transactionally. - Bind the local task only after all checks pass; otherwise remove staging, rollout, and workspace output.
API
The authenticated prefix is /api/wework-transcripts:
| Method and path | Purpose |
|---|---|
GET / | List transcripts and native segment metadata |
GET /{id} | Read one transcript |
GET /{id}/turns | Page through structured finalized-turn summaries |
GET /{id}/encryption-key | Obtain the current user's transcript cipher key |
POST /{id}/lease | Create a transcript or acquire its writer lease |
PUT /{id}/lease/{token} | Renew a lease |
POST /{id}/lease/release | Release a lease |
POST /{id}/segments | Receive ciphertext and commit index, summary, head |
POST /{id}/archive | Mark a transcript archived |
GET /{id}/archives/{archiveId}/download | Stream ciphertext through the Backend |
Object keys contain a SHA-256 digest of the transcript ID rather than the raw identifier.
Deployment configuration
Object storage reuses the ATTACHMENT_S3_* connection settings:
| Environment variable | Default | Purpose |
|---|---|---|
WEWORK_TRANSCRIPT_S3_BUCKET | wework-transcripts | Private native transcript segment bucket |
WEWORK_TRANSCRIPT_ENCRYPTION_SECRET | empty | Stable high-entropy root for per-user keys |
This design reuses the existing three transcript tables. It adds no Alembic migration and requires no schema change for existing deployments. If object storage is unavailable, segment metadata is not committed and the outbox keeps its locator while local execution remains available offline.
The transcript bucket is a separate bucket from the attachment bucket: the
account behind ATTACHMENT_S3_* needs ListBucket, GetObject, PutObject,
and DeleteObject on it. An account that is only authorized for the attachment
bucket fails on the first upload with a 503. The Backend log records the S3
error code, and the device shows the same code in parentheses at the end of the
message (for example AccessDenied).
When the dedicated root is empty, SECRET_KEY is used for compatibility.
Production deployments should configure a dedicated value and keep it unchanged
while related tgz objects are retained.
Failure semantics and troubleshooting
The settings page labels failures as Conversation upload,
Conversation download, or Preference synchronization so lease, archive
download, and preference failures are not collapsed into an unlocatable generic
error. Electron request failures preserve the underlying network cause while
redacting URL credentials from displayed messages.
If an archive index still exists in the database but its object is missing from
object storage, the download endpoint returns 404 archive_not_found. The
client logs the transcript ID, archive ID, and sequence, skips that cloud
transcript because it cannot be restored completely, and continues synchronizing
other conversations. General object-storage failures still fail the download
phase and are not mistaken for one missing archive. This behavior isolates
corrupt data; it does not fabricate or rebuild the missing object. Operators
should still use Backend logs and object-store audit records to determine why
the object was deleted.