garrytan/gstack · error · Error

Cannot rollback: ${normalized} has fewer than 2 versions in

Error message

Cannot rollback: ${normalized} has fewer than 2 versions in ${scope} scope.
Cause: no prior version to roll back to.
Action: $B domain-skill rm to delete instead, or wait for a future revision to roll back from.

What it means

Thrown by rollbackSkill() when fewer than 2 non-tombstoned version rows exist for the host in the given scope. Rollback re-emits the second-latest version as a new latest row, so it requires at least one prior version to target. With 0 or 1 versions there is nothing to roll back to.

Source

Thrown at browse/src/domain-skills.ts:385

    use_count: 0,
    flag_count: 0,
    updated_ts: now,
  };
  await appendRow(globalFile(), globalRow);
  return globalRow;
}

/**
 * Rollback to a prior version (by sha256 OR previous version number).
 * Re-emits the prior row as the latest, preserving the version counter monotonicity.
 */
export async function rollbackSkill(host: string, projectSlug: string, scope: SkillScope = 'project'): Promise<DomainSkillRow> {
  const normalized = normalizeHost(host);
  const file = scope === 'project' ? projectFile(projectSlug) : globalFile();
  const rows = await readRows(file);
  const matching = rows.filter((r) => r.host === normalized && r.scope === scope && !r.tombstone);
  if (matching.length < 2) {
    throw new Error(
      `Cannot rollback: ${normalized} has fewer than 2 versions in ${scope} scope.\n` +
        'Cause: no prior version to roll back to.\n' +
        'Action: $B domain-skill rm to delete instead, or wait for a future revision to roll back from.'
    );
  }
  // Sort by version desc; take second-latest as the rollback target.
  matching.sort((a, b) => b.version - a.version);
  const target = matching[1]!;
  const newVersion = matching[0]!.version + 1;
  const restored: DomainSkillRow = {
    ...target,
    version: newVersion,
    updated_ts: new Date().toISOString(),
  };
  await appendRow(file, restored);
  return restored;
}

View on GitHub (pinned to 94993f7401)

Solutions

  1. Use '$B domain-skill rm' to delete the skill instead if you want to remove it
  2. Wait for a future writeSkill() revision to create a second version, then rollback to the first
  3. Check version history with '$B domain-skill list' to confirm how many versions exist
Defensive patterns

Strategy: validation

Validate before calling

// Check version count before rolling back
const rows = await readRows(scope === 'project' ? projectFile(projectSlug) : globalFile());
const matching = rows.filter(r => r.host === normalizeHost(host) && r.scope === scope && !r.tombstone);
if (matching.length < 2) {
  console.error('Not enough versions to roll back. Use delete instead.');
  return;
}
await rollbackSkill(host, projectSlug, scope);

Type guard

function hasRollbackTarget(rows: DomainSkillRow[], host: string, scope: SkillScope): boolean {
  return rows.filter(r => r.host === normalizeHost(host) && r.scope === scope && !r.tombstone).length >= 2;
}

Prevention

When it happens

Trigger: Calling rollbackSkill(host, projectSlug, scope) where rows.filter(host === normalized && scope === scope && !tombstone).length < 2.

Common situations: The skill was only written once (single version), or all prior versions were tombstoned or compacted away. The user expects an undo that has no target.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/6e7ae303b6fb54dc. Report an issue: GitHub.