Skip to main content

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:

TablePurpose
wework_transcriptsTranscript identity, branch relation, sequence, state, and writer lease
wework_transcript_archivesImmutable native segment sequence, object key, SHA-256, size, and format
wework_transcript_turnsOne 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.aes256gcm snapshots. 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.aes256gcm increments.
  • 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

  1. Select the latest full snapshot at or before the current head.
  2. Download that snapshot and every contiguous later delta, verifying ciphertext SHA-256, format, and sequence.
  3. 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.
  4. Rewrite the workspace path and allocate a new thread ID on collision.
  5. Restore Codex threads and thread_dynamic_tools state transactionally.
  6. 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 pathPurpose
GET /List transcripts and native segment metadata
GET /{id}Read one transcript
GET /{id}/turnsPage through structured finalized-turn summaries
GET /{id}/encryption-keyObtain the current user's transcript cipher key
POST /{id}/leaseCreate a transcript or acquire its writer lease
PUT /{id}/lease/{token}Renew a lease
POST /{id}/lease/releaseRelease a lease
POST /{id}/segmentsReceive ciphertext and commit index, summary, head
POST /{id}/archiveMark a transcript archived
GET /{id}/archives/{archiveId}/downloadStream 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 variableDefaultPurpose
WEWORK_TRANSCRIPT_S3_BUCKETwework-transcriptsPrivate native transcript segment bucket
WEWORK_TRANSCRIPT_ENCRYPTION_SECRETemptyStable 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.