Skip to content

cbm_pipeline_pass_lsp_cross deadlocks on a specific .ts file (no progress, ~3 CPU-s, indefinite) #370

@utilia-ai-wox

Description

@utilia-ai-wox

Summary

cbm_pipeline_pass_lsp_cross enters what appears to be an unrecoverable deadlock (uninterruptible kernel state on macOS, not even kill -9 reaps the process) on at least one specific .ts file in a real-world 885-file SvelteKit + Turborepo monorepo. The pass logs pass.start pass=lsp_cross files=885, processes ~450 files in a few seconds, hits one particular file, and then stops advancing for at least 600 s (no further log lines, no CPU consumption beyond the initial ~3 s, no file/syscall activity visible in truss/fs_usage).

The process can't be killed with SIGTERM or SIGKILLps reports state UE (uninterruptible exit) and the PID survives until reboot.

Reproducer

Repository is private but the trigger file is small and pasted below. The monorepo characteristics that seem to matter:

  • ~174k total defs across ~700 modules (large all_defs[] array fed to cbm_run_ts_lsp_cross)
  • Heavy use of export * from './...' re-export chains via index.ts barrel files (workspace package entry points)
  • @<org>/<pkg> workspace imports throughout

Repro command:

codebase-memory-mcp cli index_repository '{\"repo_path\":\"/path/to/monorepo\",\"incremental\":false}' 2>&1 | tee /tmp/idx.log

The pass hangs on packages/twenty-client/src/opportunities.ts (94 lines, 3 KB), which imports two siblings (./client, ./filters, ./people) and exports two top-level async functions plus an inner toCurrency arrow.

Workaround: I added a getenv(\"CBM_DISABLE_LSP_CROSS\") guard in run_parallel_pipeline to skip the pass entirely. With it set, indexing finishes in ~1.3 s and the rest of the pipeline produces correct results (the stock 2026-04-05 binary I was previously running didn't have lsp_cross wired up at all, so this is effectively the pre-LSP behaviour).

Reproducer file

import { call, type TwentyClient } from './client';
import { quoteFilterValue } from './filters';
import { upsertContact } from './people';

type TwentyOpportunity = {
  id: string;
  [key: string]: unknown;
};

type OpportunityListResponse = { data: { opportunities: TwentyOpportunity[] } };
type OpportunityResponse = {
  data: { createOpportunity?: TwentyOpportunity; updateOpportunity?: TwentyOpportunity };
};

export type UpsertDealParams = {
  licenseId: string;
  stage: string;
  contactEmail?: string;
  contactId?: string;
  mrr?: number;
  customFields?: Record<string, unknown>;
};

export type UpsertDealResult = { twentyDealId: string };

export async function upsertDeal(
  client: TwentyClient,
  params: UpsertDealParams
): Promise<UpsertDealResult | null> {
  const existing = await findOpportunityByLicenseId(client, params.licenseId);
  const personId = await resolvePersonId(client, params);
  if (!personId && !existing) return null;
  const toCurrency = (euros: number) => ({
    amountMicros: Math.round(euros * 1_000_000),
    currencyCode: 'EUR'
  });
  const body: Record<string, unknown> = {
    stage: params.stage.toUpperCase(),
    anoLicenseId: params.licenseId,
    ...(params.mrr !== undefined ? { mrr: toCurrency(params.mrr) } : {}),
    ...(personId ? { pointOfContactId: personId } : {}),
    ...(params.customFields ?? {})
  };
  // ... (HTTP call PATCH / POST etc.)
}

(Full file ~ 94 lines, available on request.)

Observed log

level=info msg=pass.start pass=lsp_cross files=885
level=info msg=lsp_cross.file i=447 rel=packages/twenty-client/src/filters.ts lang=3
level=info msg=lsp_cross.file i=448 rel=packages/twenty-client/src/shape.test.ts lang=3
level=info msg=lsp_cross.file i=449 rel=packages/twenty-client/src/http.ts lang=3
level=info msg=lsp_cross.file i=450 rel=packages/twenty-client/src/people.ts lang=3
level=info msg=lsp_cross.file i=451 rel=packages/twenty-client/src/opportunities.ts lang=3
# 600+ s of silence after this line

Per-file log was added locally to localise the hang (one cbm_log_info call at the start of the for-loop body in cbm_pipeline_pass_lsp_cross).

Environment

  • macOS 15.x (Darwin 25.3.0), Apple Silicon (arm64)
  • Built with stock toolchain (Apple Clang) via make -f Makefile.cbm cbm on commit 6226972
  • 16 GB RAM, default mem budget (8 GB)
  • File packages/twenty-client/src/opportunities.ts: 94 lines, plain TypeScript, no decorators, three relative imports

What I'd suggest investigating

  • pxc_run_one_ts allocates a CBMArena scratch and calls cbm_run_ts_lsp_cross with the full all_defs[] (174k entries here). Maybe the type registry build is O(N²) on the re-export chain depth, or a cycle in export * from cross-references confuses the LSP resolver.
  • A timeout / watchdog inside cbm_run_ts_lsp_cross (e.g. bail with a lsp_cross.timeout log line after N seconds) would degrade gracefully instead of hanging the whole indexing pass.
  • Less ambitiously: skip files where def_count * file_count > 10^8 (or similar O(N²) gate) to avoid pathological inputs entirely.

I can't share the proprietary repo but I'm happy to bisect what part of opportunities.ts is triggering the deadlock if you can suggest specific patterns to test (e.g. type aliases vs interfaces, async arrow inside async function, etc.).

Workaround docs

The CBM_DISABLE_LSP_CROSS=1 environment guard I added is part of the workaround patch in #369 (well, actually it's not in that PR — it's a separate local change). Happy to extract that as a small additional PR if you'd like a documented escape hatch in upstream.

Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions