```ts
/**
 * Adds two non-negative decimal strings of arbitrary length, handling carry manually.
 * Throws an error if inputs are invalid (non-string, empty, or contain non-digit characters).
 */
export function addDecimalStrings(a: string, b: string): string {
  // Validate inputs
  if (typeof a !== 'string' || typeof b !== 'string') {
    throw new Error('Inputs must be strings');
  }

  if (a.length === 0 || b.length === 0) {
    throw new Error('Inputs must not be empty');
  }

  if (!/^\d+$/.test(a) || !/^\d+$/.test(b)) {
    throw new Error('Inputs must contain only digits 0-9');
  }

  // Remove leading zeros (but keep at least one digit for "0")
  const normalize = (s: string): string => {
    let i = 0;
    while (i < s.length - 1 && s[i] === '0') i++;
    return s.slice(i);
  };

  const aNorm = normalize(a);
  const bNorm = normalize(b);

  // Ensure a is the longer string for simpler processing
  if (aNorm.length < bNorm.length) {
    [aNorm, bNorm] = [bNorm, aNorm];
  }

  let carry = 0;
  let result: string[] = [];
  
  // Process digits from right to left
  for (let i = aNorm.length - 1, j = bNorm.length - 1; i >= 0 || j >= 0; i--, j--) {
    const digitA = i >= 0 ? parseInt(aNorm[i], 10) : 0;
    const digitB = j >= 0 ? parseInt(bNorm[j], 10) : 0;
    
    const sum = digitA + digitB + carry;
    carry = Math.floor(sum / 10);
    result.push((sum % 10).toString());
  }

  // Add final carry if present
  if (carry > 0) {
    result.push(carry.toString());
  }

  // Reverse and join result
  const finalResult = result.reverse().join('');

  // Handle the special case where result is "0"
  return finalResult === '' ? '0' : finalResult;
}
```