{"record":{"id":"9c739822809c3287","repo":"thedotmack/claude-mem","slug":"syncclient-requires-a-non-empty-huburl-claude-mem-cloud-sync","errorCode":null,"errorMessage":"SyncClient requires a non-empty hubUrl (CLAUDE_MEM_CLOUD_SYNC_HUB_URL)","messagePattern":"SyncClient requires a non-empty hubUrl \\(CLAUDE_MEM_CLOUD_SYNC_HUB_URL\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncClient.ts","lineNumber":256,"sourceCode":"  private backoffMs = 0;\n  private failStreak = 0;\n  private failCursor: string | null = null;\n\n  // Advisory socket state (all of it disposable — prime directive #2).\n  private socket: SyncSocketLike | null = null;\n  private socketLive = false;\n  private wsAttempts = 0;\n  private pingTimer: ReturnType<typeof setInterval> | null = null;\n  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n  /** True while the hub says X-Sync-Mode: poll (kill switch tripped). */\n  private pollModeOnly = false;\n  /** True while the pull loop is suspended (socket torn down with it). */\n  private suspended = false;\n\n  constructor(apply: SyncApply, options: SyncClientOptions) {\n    const hubUrl = (options.hubUrl ?? '').trim().replace(/\\/+$/, '');\n    if (!hubUrl) {\n      throw new Error('SyncClient requires a non-empty hubUrl (CLAUDE_MEM_CLOUD_SYNC_HUB_URL)');\n    }\n    if (!options.deviceId) {\n      // Same fail-closed posture as CloudSync/SyncApply: pulling without an\n      // identity would mis-classify our own echoes.\n      throw new Error('SyncClient requires a non-empty deviceId (use the CloudSync-resolved id)');\n    }\n    this.apply = apply;\n    this.hubUrl = hubUrl;\n    this.token = options.token ?? '';\n    this.userId = options.userId ?? '';\n    this.deviceId = options.deviceId;\n    this.deviceName = (options.deviceName ?? '').trim().slice(0, 80);\n    this.fetchImpl = options.fetchImpl ?? globalThis.fetch;\n    this.activePollMs = options.activePollMs ?? 30_000;\n    this.idlePollMs = options.idlePollMs ?? 300_000;\n    this.suspendAfterMs = options.suspendAfterMs ?? 3_600_000;\n    this.pageLimit = options.pageLimit ?? 500;\n    this.maxPagesPerCycle = options.maxPagesPerCycle ?? 40;","sourceCodeStart":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/SyncClient.ts#L238-L274","documentation":"The SyncClient constructor trims and strips trailing slashes from options.hubUrl and requires a non-empty result, naming the CLAUDE_MEM_CLOUD_SYNC_HUB_URL env var since that is the usual source. Without a hub URL the realtime pull loop has nowhere to connect, so construction fails closed. A missing deviceId similarly throws immediately after.","triggerScenarios":"new SyncClient(apply, { hubUrl: undefined | '' | '   ', deviceId }) — typically when CLAUDE_MEM_CLOUD_SYNC_HUB_URL is unset/empty and the caller passes it through unchecked.","commonSituations":"Env var not exported in shell/service context; settings.json lacks the sync hub URL; value set only to whitespace; constructing SyncClient in tests without options.","solutions":["Set CLAUDE_MEM_CLOUD_SYNC_HUB_URL to the hub base URL (e.g. https://sync.example.com).","Confirm the env var is actually visible to the worker process (export it, restart the service).","Validate hubUrl before constructing: if (!url?.trim()) skip/queue client creation.","Also ensure deviceId is provided (the very next check throws otherwise)."],"exampleFix":"// before\nconst client = new SyncClient(apply, { hubUrl: process.env.CLAUDE_MEM_CLOUD_SYNC_HUB_URL, deviceId });\n// after\nconst hubUrl = process.env.CLAUDE_MEM_CLOUD_SYNC_HUB_URL;\nif (!hubUrl?.trim()) throw new Error('CLAUDE_MEM_CLOUD_SYNC_HUB_URL must be set');\nconst client = new SyncClient(apply, { hubUrl, deviceId });","handlingStrategy":"validation","validationCode":"const hubUrl = process.env.CLAUDE_MEM_CLOUD_SYNC_HUB_URL;\nif (!hubUrl || !hubUrl.trim()) throw new Error('CLAUDE_MEM_CLOUD_SYNC_HUB_URL is required');\nconst client = new SyncClient(apply, { hubUrl, deviceId });","typeGuard":null,"tryCatchPattern":"try { client = new SyncClient(apply, opts); } catch (e) { if (e.message.includes('non-empty hubUrl')) { /* surface missing-env-var config error to user */ } }","preventionTips":["Document required env vars and check them at boot.","Trim/normalize env values before passing to constructors.","Fail fast with a clear message instead of passing undefined through."],"tags":["sync","config","constructor"],"backgroundTag":"missing-env-var","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}