slopus/happy · warning · Error

Metadata version mismatch

Error message

Metadata version mismatch

What it means

Thrown by updateMachineMetadata when the server responds 'version-mismatch' for machine metadata. Before throwing, the client adopts the server's newer version and metadata (if answer.version is higher) so the next attempt converges; the throw itself is deliberate — it signals the optimistic update conflicted and triggers the caller's retry logic.

Source

Thrown at packages/happy-cli/src/api/apiMachine.ts:394

        await backoff(async () => {
            const updated = handler(this.machine.metadata);

            const answer = await this.socket.emitWithAck('machine-update-metadata', {
                machineId: this.machine.id,
                metadata: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
                expectedVersion: this.machine.metadataVersion
            });

            if (answer.result === 'success') {
                this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
                this.machine.metadataVersion = answer.version;
                logger.debug('[API MACHINE] Metadata updated successfully');
            } else if (answer.result === 'version-mismatch') {
                if (answer.version > this.machine.metadataVersion) {
                    this.machine.metadataVersion = answer.version;
                    this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
                }
                throw new Error('Metadata version mismatch'); // Triggers retry
            }
        });
    }

    /**
     * Update daemon state (runtime info) - similar to session updateAgentState
     * Simplified without lock - relies on backoff for retry
     */
    async updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void> {
        await backoff(async () => {
            const updated = handler(this.machine.daemonState);

            const answer = await this.socket.emitWithAck('machine-update-state', {
                machineId: this.machine.id,
                daemonState: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
                expectedVersion: this.machine.daemonStateVersion
            });

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Let the retry logic re-run updateMachineMetadata — the client already adopted the server's newer metadata
  2. Ensure only one daemon instance per machine is running
  3. If mismatches persist, refresh machine state from the server and reapply local edits
Defensive patterns

Strategy: retry

Try / catch

try {
  await updateMachineMetadata(metadata);
} catch (e) {
  if (e.message === 'Metadata version mismatch') {
    // client already adopted server version; safe to retry
    await updateMachineMetadata(metadata);
  } else throw e;
}

Prevention

When it happens

Trigger: Two writers (e.g., this daemon and another instance/machine process) update machine metadata concurrently; the local metadataVersion is stale relative to the server's.

Common situations: Running multiple daemons for the same machine; a daemon restarted while a stale keep-alive loop from the previous run was still retrying; rapid successive metadata updates racing.

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/1c4475d46a7bd932. Report an issue: GitHub.