koala73/worldmonitor · error · Error
batch rows must share one clientImportId
Error message
batch rows must share one clientImportId
What it means
Thrown by normalizeCompanyImportBatch() when, after per-row normalization and sorting, any row carries a clientImportId different from the first row's. The contract requires every row in one batch to share a single clientImportId because that ID identifies the whole import session (idempotency and attribution unit), not individual rows.
Source
Thrown at shared/company-monitoring-contract.ts:318
if (utf8Bytes(JSON.stringify(normalized)) > COMPANY_MONITORING_LIMITS.maxImportRowBytes) {
throw new Error(`row exceeds ${COMPANY_MONITORING_LIMITS.maxImportRowBytes} bytes`);
}
return normalized;
}
export function normalizeCompanyImportBatch(inputs: CompanyImportRowInput[]): NormalizedCompanyImportRow[] {
if (!Array.isArray(inputs)) throw new Error('import batch must be a list');
if (inputs.length === 0) throw new Error('import batch requires at least one row');
if (inputs.length > COMPANY_MONITORING_LIMITS.maxImportRows) {
throw new Error(`batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportRows} rows`);
}
const normalized = inputs.map(normalizeCompanyImportRow).sort((left, right) => left.ordinal - right.ordinal);
const importId = normalized[0]?.clientImportId;
for (let index = 0; index < normalized.length; index += 1) {
const row = normalized[index]!;
if (row.clientImportId !== importId) throw new Error('batch rows must share one clientImportId');
if (row.ordinal !== index) throw new Error('batch ordinals must be contiguous from 0');
}
if (utf8Bytes(JSON.stringify(normalized)) > COMPANY_MONITORING_LIMITS.maxImportBatchBytes) {
throw new Error(`batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportBatchBytes} bytes`);
}
return normalized;
}
export function assertCompanyMonitoringPayloadSize(byteLength: number): void {
if (!Number.isSafeInteger(byteLength) || byteLength < 0) throw new Error('request byte length is invalid');
if (byteLength > COMPANY_MONITORING_LIMITS.maxRequestBytes) {
throw new Error(`request exceeds ${COMPANY_MONITORING_LIMITS.maxRequestBytes} bytes`);
}
}
export function assertCompanyMonitoringAccountContext(
context: { ownerAccountId?: string } | null | undefined,
): string {View on GitHub (pinned to eeab0a219f)
Solutions
- Generate one clientImportId (e.g. crypto.randomUUID()) once per batch and stamp it onto every row before calling normalizeCompanyImportBatch()
- Pre-validate: const id = rows[0]?.clientImportId; assert rows.every(r => r.clientImportId === id)
- When retrying or resuming an import, keep rows from different sessions in separate calls rather than concatenating
Example fix
// before
const rows = inputs.map((input, i) => ({ ...input, ordinal: i, clientImportId: crypto.randomUUID() }));
const normalized = normalizeCompanyImportBatch(rows); // every row has a different ID -> throws
// after
const clientImportId = crypto.randomUUID(); // one ID for the whole batch
const rows = inputs.map((input, i) => ({ ...input, ordinal: i, clientImportId }));
const normalized = normalizeCompanyImportBatch(rows); Defensive patterns
Strategy: validation
Validate before calling
const clientImportId = crypto.randomUUID();
const rows = rawRows.map((r, i) => ({ ...r, ordinal: i, clientImportId }));
const shared = rows.length === 0 || rows.every(r => r.clientImportId === rows[0]!.clientImportId);
if (!shared) throw new Error('rows carry mixed clientImportId — regenerate before submit'); Type guard
function hasSingleImportId(rows: { clientImportId: string }[]): boolean {
const first = rows[0]?.clientImportId;
return first !== undefined && rows.every(r => r.clientImportId === first);
} Try / catch
catch (e) { if (e instanceof Error && e.message === 'batch rows must share one clientImportId') { rebuildBatchWithOneId(); } else throw e; } Prevention
- Create clientImportId once per batch, before the per-row map — never inside it
- Never concat rows from different import sessions; send separate calls
- Assert the single-ID invariant in the import client's unit tests
When it happens
Trigger: Concatenating rows prepared for two different import sessions into one call; generating a fresh clientImportId per row instead of once per batch; retry logic that mixes leftover rows from a previous partial import with new rows.
Common situations: Client code that creates the ID inside the per-row mapping function instead of once before the loop; merging 'retry the failed rows' with 'continue with new rows' in one request; a CSV uploader that restarts ID generation mid-file after an error.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportRows} row
- batch ordinals must be contiguous from 0
- batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportBatchByte
- invalid ${kind} logical ID
- get_intel_timeline requires at least one of domain ("conflic
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/ffa077ceb21f05e3.
Report an issue: GitHub.