You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
cross-repo-intelligence: --target-projects flag silently parses to zero targets; with raw-JSON args the worker crashes (0-byte log) #1133
Environment: v0.9.0, Windows amd64 (ui variant). Source project: Python/FastAPI + Svelte monorepo (~31k nodes, 928 intra-repo HTTP_CALLS edges). Target project: Python legacy monorepo (~13.5k nodes) whose REST API the source calls over HTTP — so real cross-repo matches should exist.
Repro 1 — flag form, silent no-op:
cli index_repository --repo-path <A> --mode cross-repo-intelligence --target-projects '["<project-B>"]'
# → {"status":"success","projects_scanned":0,"total_cross_edges":0,"elapsed_ms":2.9}
# Same result with --target-projects '["*"]'
The array flag value is apparently dropped: projects_scanned: 0 plus an instant "success". An unknown or unparseable target should be an explicit error — silent-zero results mislead LLM agents (same family as #1111).
Repro 2 — raw JSON args, worker crash (2/2):
echo '{"repo_path":"<A path>","mode":"cross-repo-intelligence","target_projects":["<project-B>"]}' | cli index_repository
# → {"status":"error","outcome":"exit_nonzero","hint":"Indexing worker crashed on a file. The crash was contained (the server survived). Re-run to retry; a future release isolates the culprit file."}
# Reproducible with ["*"] too. Worker log: 0 bytes (see my resource-guards issue).
So the flag form never reaches the matcher, and the JSON form reaches it and crashes. The source project's existing index survived intact in all attempts (verified via index_status).
Asks: fix array parsing in the flag form, error out on zero resolved targets instead of returning success, and include the culprit file in the crash hint.
Thanks for separating the flag-path no-op from the raw-JSON crash. This is a high-priority 0.9.1-rc correctness and containment bug. The RED cases should independently prove that --target-projects preserves a JSON array, zero resolved targets fail explicitly rather than returning success, and equivalent raw-JSON input cannot crash the worker. The existing project index must remain intact in every failure case; the 0-byte worker-log problem is tracked in #1132.
Confirming Repro 1 on a different setup, and reporting a different outcome for Repro 2 — the stdin form does not crash here, which suggests your crash is orthogonal to the flag-parsing bug.
Environment: v0.9.0, Windows amd64, standard variant (you were on ui), installed via npm.
Minimal shareable repro — two throwaway TypeScript projects, ~50 LOC total, no node_modules needed:
# (A) flag form
$ cli index_repository --repo-path <repro>/web-client --name web-client \ --mode cross-repo-intelligence --target-projects '["*"]'{"status":"success","projects_scanned":0,"total_cross_edges":0}
# (B) same arguments, piped as JSON on stdin
$ echo'{"repo_path":"<repro>/web-client","name":"web-client","mode":"cross-repo-intelligence","target_projects":["*"]}' \ | cli index_repository{"status":"success","projects_scanned":2,"total_cross_edges":0}
projects_scanned: 0 → 2. Only the argument-passing form changed, so the flag path never reaches the matcher — same conclusion as yours, now also on the standard variant, on TS/JS instead of Python, and at ~48 nodes instead of ~31k. So it is neither language- nor scale-dependent.
On your Repro 2: piping JSON on stdin is stable here — no worker crash in 7 invocations of the stdin form across 6 distinct projects: the two fixtures above (39 and 9 nodes) and four real TypeScript/NestJS/Next.js repos (516 / 614 / 3,289 / 3,678 nodes). All returned "status":"success" with projects_scanned ≥ 2. So the stdin form looks like a usable workaround, and the crash you hit is likely a separate file-specific indexing failure rather than part of this bug (possibly the same root as #1379). Worth splitting so the parser fix is not blocked behind the crash.
Why the silent-success part matters more than the parsing itself: our setup script had been running exactly the (A) form on every bootstrap and every repo-add for weeks. projects_scanned: 0 with "status":"success" and exit 0 meant the cross-repo pass had never once executed, and nothing in the output or exit code said so. We only found it while investigating why cross-repo edges were always empty — and the empty result was itself indistinguishable from "there genuinely are no cross-service calls". Strong +1 on erroring out when zero targets resolve; an agent consuming this output cannot tell success-with-no-work from success-with-work.
(Separately: --target-projects <array> … "Use ["*"] for all indexed projects" in cli index_repository --help documents precisely the form that fails, so the help text points users at the broken path.)
@HungChu thanks — the Repro 1 corroboration is the useful kind, because it moves three axes at once relative to our report: standard variant vs our ui, TS/JS vs our Python, and ~48 nodes vs our ~31k. That removes variant, language and scale as explanations for the flag-path no-op. Agreed on splitting, and I'll add one reason below that got stronger since we filed.
I can independently confirm your last point on the pinned v0.9.0 ui binary — the help text does document precisely the failing form:
--target-projects <array> Projects to search for cross-repo links (cross-repo-intelligence mode).
Use ["*"] for all indexed projects. Run list_projects to see available projects.
On Repro 2, one caveat about what your run establishes. I think your conclusion is probably right, but the measurement doesn't carry it yet. Your largest stdin invocation was 3,678 nodes; our crash was a ~31k-node source project against a ~13.5k-node target — about 8.5× above your maximum — and it reproduced 2 out of 2 attempts.
To be precise about what we saw, since it bears on the link to #1132: those two were contained worker crashes — the worker died, the existing graph database was left intact — not the OS-level freezes reported separately in that issue. What the two classes share is the 0-byte log signature: the worker is gone before a single line is flushed, which is what makes both of them RAM/IO-exhaustion candidates and why we filed them as one containment story rather than as the same event. A RAM-bound failure is scale-dependent by construction, so "no crash across 7 invocations at ≤3.7k nodes" is the expected observation whether the crash is orthogonal to the parser bug or sits in the same path.
Concretely falsifiable, if anyone wants to settle it: run the stdin form with a source project of ≥30k nodes against a target of ≥10k. If that returns "status":"success" with projects_scanned ≥ 2, orthogonality is established and #1132's containment work is genuinely unrelated to this issue. If it dies with a 0-byte log, the two are the same containment problem seen from different sides. I can run that side on Windows if it helps; I have the 31k/13.5k pair indexed.
Worth being precise about one sentence, only because a reader skimming might over-apply it: "neither language- nor scale-dependent" holds for Repro 1, where you demonstrated it. Extending it to the crash would discount #1132's framing, which is the opposite of what your data shows.
The new argument for splitting: this issue has since moved from 0.9.1-rc to 0.10.0-rc, along with #1132, #1196, #480, #1351 and #1206. The flag-parsing half is deterministic, has an obvious RED case, and needs no containment design — it does not need to wait on a crash whose reproduction is still contested. On #1379 as a possible shared root: plausible, but it is macOS, reports "all modes affected" rather than cross-repo specifically, and has been sitting in awaiting-reporter since 07-31. Tying the parser fix to it is a bad trade in either direction.
Strong +1 on erroring out when zero targets resolve, and your framing of why is the part I'd want in the acceptance criteria. Our situation was the mirror image of yours: we caught it during a one-off validation on day one and blocked the mode in our indexing wrapper, so it never entered our pipeline. You ran the documented form on every bootstrap for weeks and the output could not tell you. projects_scanned: 0 + "status":"success" + exit 0 is indistinguishable from "these services genuinely share no routes" — which is a plausible real answer, and that is exactly what makes the silent version dangerous. An agent consuming this cannot form the doubt that would make it check.
I ran the test I offered above, and the result corrects my own previous comment as much as it does anything else. Repro 2 is nondeterministic. Both our original "crashes 2/2" and @HungChu's "stable across 7 invocations" are samples of a flaky failure, reported by each of us as if it were deterministic.
Environment: v0.9.0, Windows amd64, ui variant, pinned binary, pre-existing cache. Source kumo-app (31,287 nodes / 176,178 edges), target webIA (13,462 / 46,501) — i.e. ~8.5× the largest project in @HungChu's run, which was the scale gap I pointed at earlier.
25 invocations, same binary, same store
launch context
success
crash
target
bash script, backgrounded, ~3s gaps
4
0
["webIA"]
PowerShell, mixed cadence
5
1
["webIA"]
tight bash loop
0
4
["webIA"]
tight bash loop (bisect)
0
9
9 different targets
single
0
1
["*"]
total
9
16
For the byte-identical payload (target_projects: ["webIA"]): 16 invocations → 9 success, 7 crash.
So the crash does reproduce at ≥30k nodes — my scale hypothesis was wrong about the mechanism, though right that your run didn't settle the question.
What I ruled out, each with data rather than reasoning
Not the argument form. The stdin form both succeeds and crashes.
Not the target project.["webIA"] both succeeds and crashes with an identical payload.
Not scale. 8.5× your maximum, and it completes in ~9s at a 65MB peak.
Not stdout redirection. Crashes identically whether stdout is a file or a pipe.
Not sticky state. After 13 consecutive failures, the same script succeeded again immediately; the graph store was intact throughout.
Not cadence, at least not simply. A 20–25s cooldown produced successes, but two back-to-back invocations also succeeded immediately after a crash.
The one correlation I could not explain away: every invocation launched from a tight foreground bash loop failed (13/13), while the backgrounded script and PowerShell succeeded 9 of 12. I am explicitly not claiming a mechanism there — with a ~56% per-call success rate, 13 consecutive failures is unlikely (p ≈ 0.001) but I could not isolate what differs beyond the loop itself.
Every failure returns the contained-crash envelope and leaves a 0-byte logs/.worker-*.log — the same signature as #1132. I counted ten. The graph store survived all 16 crashes: 10 projects intact, node/edge counts stable, query_graph returning correct rows throughout.
The part that matters for the feature, not just the bug
When it succeeds, cross-repo-intelligence works. All 9 successful runs returned identical results — 117 cross-repo HTTP edges between the two projects — and they were idempotent: edge totals settled at 176,218 / 46,588 and did not inflate across repeated runs.
That changed our posture. We had blocked the entire mode in our indexing wrapper since July on the strength of a 2/2 crash. On this evidence the flag-path no-op is the part that is genuinely broken and silent; the stdin path is usable with retry, and we are unblocking it accordingly.
Suggested consequence for this issue
The split @HungChu proposed looks right, and the flakiness is an argument for it rather than against: the flag-parsing no-op is deterministic, has an obvious RED case, and is testable in isolation — whereas the crash needs a flaky-test harness (repeat N times, assert zero contained-crash envelopes) plus whatever makes the worker die without flushing a single log line. Blocking the first on the second means neither ships.
If it helps, I can run a fixed-N repetition harness against any candidate build and report the success/crash ratio, since I appear to have an environment that reproduces it about 4 times out of 10.
Correction to my previous comment. I wrote that "when it succeeds, cross-repo-intelligence works," citing 117 edges as evidence. I checked those edges afterwards and that claim does not hold. The crash matrix stands; the working-feature claim does not.
What the edges actually are
Of the 40 CROSS_HTTP_CALLS written into the source project's graph, 3 point at httpbin.org/ip (health-check URLs — separate noise) and the remaining 37 span 20 distinct routes. I spot-checked 7 of those routes. Every one is defined inside the calling project itself.
The most frequent target, /api/proveedores, resolves cross-repo to the target project — while the caller's own backend contains:
Same for /api/clientes-externos, /api/facturas, /api/tasks, /api/empresas, /api/sedes, /api/horarios — each defined in 1–7 files of the caller's own backend. The callers are SvelteKit frontends living in the same repository as that backend, doing fetch('/api/proveedores') against their own API. There is no HTTP client to the other project at all: the config variable holding its base URL has zero consumers in the codebase.
So these are path-string collisions, not cross-service calls. Our pair is close to a worst case for path-based matching — one project is the in-progress migration of the other, so they share route paths by construction. But the failure does not require that: it needs only two indexed projects that both expose a route with the same path, which is unremarkable for /api/tasks or /oauth/token.
The part I think is a real, separate bug
The matcher emits a cross-repo edge even when the caller's own project defines the same route. Nothing in the output distinguishes "this call demonstrably leaves the service" from "some other indexed project happens to expose this path too" — and the second is silently reported as the first.
That matters more than a wrong edge count, because the mode's output is the kind an agent consumes without a way to doubt it. total_cross_edges: 117 reads as an architectural fact.
Two things that would fix it, in order of preference:
Prefer the caller's own project when resolving a route, and emit a cross-repo edge only when the path is not defined locally.
Failing that, mark the ambiguity on the edge. The machinery already exists elsewhere in the schema — CALLS edges carry confidence, candidates and strategy. A CROSS_HTTP_CALLS edge that had candidates: 2 would let a consumer filter, instead of having to reverse-engineer route ownership by hand as I just did.
Happy to open this as its own issue rather than let it ride here, given we have all been arguing for splitting this one — say the word and I will file it with the repro above.
What still stands from my previous comment
The nondeterminism is unchanged and is independent of this: 16 invocations of a byte-identical payload → 9 success / 7 contained crash, each failure leaving a 0-byte logs/.worker-*.log. The successful runs were deterministic and idempotent in output — they consistently produced the same 117 edges and did not inflate on repetition. Those edges just do not mean what the field name says.
@junk151516 your criticism of my Repro 2 inference was correct, and your 25-run matrix settles it. I over-read seven non-crashing runs at ≤3.7k nodes as evidence of orthogonality, when all it supported was "does not reproduce at this scale". Calling the crash "a separate file-specific indexing failure" was a claim my data could not carry — and pointing at #1379 was the weakest part, for the reasons you gave. Withdrawn.
Since a flaky failure is now the working model, I ran the one cell your matrix could not fill: the same tight foreground bash loop, at small scale. That was your single unexplained correlation (13/13 failures from a tight loop, 9/12 successes from backgrounded/PowerShell), and scale was the obvious confound.
Environment: v0.9.0, Windows amd64, standard variant, npm install, pre-existing shared cache, 5 indexed projects. Source project = the largest in our fleet, 3,678 nodes / 9,823 edges. No cooldown between calls; sequential in one for loop in one shell.
So the tight-loop correlation does not reproduce at 3.7k nodes. Combined with your data, the loop alone is not sufficient — something scale-dependent has to be present too, which is consistent with the RAM/IO-exhaustion framing in #1132 and inconsistent with "the loop is the mechanism". That narrows it in a useful direction, though obviously it cannot prove where the memory goes.
Graph integrity after the 15 runs: all 5 projects intact, node/edge counts unchanged on the source project (no edge inflation across repeated runs — matching your idempotence observation), search_graph returning correct rows. So the containment holds at this scale too.
On your false-edge correction — please do file it separately, and I would treat it as the most consequential finding in this thread. Your pair shares route paths by construction, but as you say the precondition is only "two indexed projects both exposing the same path", and that is ordinary. Concretely: our fleet is four NestJS services behind a convention where every business controller serves /api/v1/<resource>, with a shared Postgres and overlapping resource vocabulary between the service that admits records and the two that read them. Path-only matching would collide across those three by construction, not by accident.
There is an ordering hazard worth naming in that new issue, because it is not obvious: I filed #1428 (NestJS @Controller/@Get decorators produce zero Route nodes). Today that gap makes our fleet immune to your false-edge bug — no Route nodes, no collisions, total_cross_edges: 0. If the extraction gap is fixed before route-ownership resolution, every affected fleet flips straight from "silently zero" to "silently wrong", with no intermediate state where anyone would notice. Zero edges at least looks like nothing happened; 117 plausible-looking edges read as an architectural fact. Your preference (1) — prefer the caller's own project, emit cross-repo only when the path is not defined locally — should probably land before or with any new framework's route extraction, not after.
Your candidates/confidence fallback also seems worth having regardless of (1), for the genuinely ambiguous case where two projects both define a path and the caller defines neither.
I do not have a ≥30k-node project to contribute to the crash-repetition side, so I cannot help there — but if a candidate build lands, I can run the small-scale cell (3.7k, tight loop, N=15+) as a regression check that the flag-parsing fix does not introduce a crash at scales that were previously clean.
@HungChu — appreciated, and the small-scale loop cell is a genuine addition. Three things: one correction to how it's labelled, one comparison from our matrix that I think you should have before the loop gets written off, and agreement on the ordering hazard, which I think is the most important thing either of us has posted in this thread.
Your cell establishes less than "scale-dependent", and more than you claimed elsewhere
The within-environment part of your test is clean and I have no quarrel with it: 7 non-loop and 15 loop invocations, same binary, same variant, same fleet, same 3,678-node source — no crash either way. Against the strong reading "the tight loop is the mechanism", that is decisive. P(0 crashes in 15 | rate = 100%) = 0. That reading is dead and I am not defending it.
What I do not think it supports is the label scale-dependent. The comparison that carries that conclusion is your setup against ours, and that varies on at least five axes at once:
ours
yours
variant
ui
standard
language / framework
Python
TS/JS + NestJS
indexed projects in store
10–11
5
host
—
—
source project
31,287 nodes
3,678 nodes
The defensible statement is setup-dependent, with scale as the leading candidate. I am pressing on this only because it is the same failure mode that has now bitten this thread three times — twice by me — and each time it looked exactly this reasonable: two or more variables moved, and the effect got assigned to the most interesting one.
One statistical footnote, since it affects how the cell should be cited later: 0/15 bounds your crash rate at ≤18.1% (95%). It rules out 100%, and it rules out our observed 44%, but it does not establish zero. "Does not reproduce at 3.7k" should not harden into "3.7k is clean" in anyone's head, including mine.
The comparison that discriminates is already in our matrix
You wrote that the loop correlation was the cell my matrix could not fill. It could not fill it at your scale — but the split at our scale is in the same 25 invocations:
So at 31k the invocation method has a large effect, and your 0/15 at 3.7k does not touch that. Taken together, the model consistent with both datasets is not "the loop is irrelevant" — it is an interaction: the loop matters above some threshold and not below it. Your phrasing ("inconsistent with 'the loop is the mechanism'") is strictly correct, but it reads as demoting the loop, and it is still the strongest single predictor either of us has. If it gets dropped as a variable in whatever harness runs against a candidate build, we lose the one lever that reliably moved the outcome.
⚠️Caveat on my own number, stated up front rather than after someone catches it: that 13/13 vs 3/12 split spans all 25 invocations, whereas the byte-identical-payload subset was the 16 that gave 9 success / 7 crash. I no longer have the raw log to re-derive the split restricted to byte-identical runs, so treat p ≈ 1.1e-4 as indicative, not as a clean controlled contrast. If someone reproduces this, log the payload hash per run — I did not, and that is why this caveat exists.
The experiment that would actually settle it
You say you have no ≥30k-node project to contribute. You do not need one of yours. Index a large third-party TypeScript repo — the NestJS monorepo itself, the TypeScript compiler, Nx — until you clear ~30k nodes, then run the same tight loop on your own host with your own variant.
That converts the scale question from a cross-environment comparison into a ladder inside a single environment, which is worth considerably more than the cell you just filled, and it is the one design where scale is the only thing moving. If the loop crashes at 30k on your box, scale is confirmed and the variant/language axes are eliminated. If it does not, the difference is in our setup and I should go find it here.
Your framing of the small-scale regression cell is right, and worth keeping as-is: it checks that a flag-parsing fix does not introduce a crash where none existed. It cannot speak to the flaky rate, and you did not claim it could.
The ordering hazard — agreed, and it is not hypothetical
This is the part I would prioritise, and I want to add the piece that I think makes it land.
You present the flip from "silently zero" to "silently wrong" as what would happen if #1428 is fixed before route-ownership resolution. It is not conditional. Our two fleets are the two states of the same bug, right now:
Yours — NestJS, routes not extracted, no Route nodes, no collisions: total_cross_edges: 0. Silently zero.
Ours — FastAPI, routes extracted: total_cross_edges: 117. Of the 40 CROSS_HTTP_CALLS attributed to kumo-app, 3 are obvious noise (httpbin.org/ip) and 37 are path collisions across 20 distinct routes. We hand-audited 7 of those routes: 7 of 7 are defined by the calling project itself, and 0 verified cross-service calls survived. Silently wrong, already shipped, for weeks.
Nobody has to wait for #1428 to see the other side of the transition. We are standing on it. And we only caught it because a CLAUDE.md in that repo asserts there is no HTTP client to the legacy service, which contradicted the graph. Absent that contradiction sitting in the same repo, nobody audits a field called total_cross_edges — it reads as an architectural fact, and nothing in the output distinguishes "this call leaves the service" from "another indexed project happens to expose the same path".
One generalisation worth putting in the acceptance criteria: this is not a NestJS concern. Any improvement to route extraction, for any framework, flips every affected fleet from the first state to the second, with no intermediate state where a user would notice. So your preference (1) — resolve to the caller's own project, emit a cross-repo edge only when the path is not defined locally — is not a dependency of #1428 specifically. It is a precondition for the route-extraction roadmap as a whole.
Your candidates/confidence fallback is worth having independently, for the genuinely ambiguous case where two projects define a path and the caller defines neither. The machinery already exists — CALLS edges carry confidence/candidates/strategy — so this is plumbing an existing field through a new edge type, not new infrastructure.
Filed as #1459, with the mechanism, our 7/7 audit, and the ordering argument credited to you.
I cited your fleet as the primary precondition example, and I want to be explicit about why rather than have it look like borrowing your evidence: our pair shares routes by construction — the source repo is the in-progress migration of the target — so it is close to a worst case for a path matcher, and it is easy to dismiss on exactly that basis. Four services behind /api/v1/<resource> with an overlapping resource vocabulary is just an ordinary convention. "This happens to a normal fleet" is a much stronger argument than "this happens to our unusual one". Say the word if you would rather it be described differently and I will edit it.
You are right on all three, and the labelling one is the same mistake you flagged earlier in this thread — I assigned the effect to the most interesting variable while five had moved. "Setup-dependent, with scale as the leading candidate" is the correct reading of my cell; I withdraw "scale-dependent". Two axes I had not even listed as differing: store size (your 10–11 indexed projects vs our 5) and variant.
Accepting the other two without qualification:
0/15 bounds the crash rate at ≤18.1% (95%), it does not establish zero. It excludes 100% and excludes your observed 44%; that is all. I will cite it that way and not let it harden into "3.7k is clean".
The loop should not be demoted. Your 13/13 vs 3/12 (p ≈ 1.1e-4) is at your scale, my 0/15 is at mine, and the model consistent with both is an interaction — the loop matters above some threshold, not nowhere. My phrasing read as demotion and that was wrong. Concretely: any harness run against a candidate build should keep invocation method as a variable, not standardise on backgrounded invocation and declare the crash fixed. That is the failure mode your point protects against, and it is a real risk if someone reads only my cell.
Your self-caveat about the split spanning all 25 rather than the byte-identical 16 is noted, and the lesson is worth stating for whoever reproduces this: log a payload hash per run. Your p is indicative, and the reason it cannot be tightened is a logging gap, not a data gap.
On the ladder experiment
The design is right, and it is clearly the experiment that discriminates — a single environment with scale as the only moving variable beats any cross-environment comparison, including mine. I am not going to promise a date for it: it means cloning a large third-party repo and driving an indexer to ~30k nodes on a corporate work machine whose network already blocks GitHub release assets, and that is a resource decision I do not own unilaterally. If I run it I will post the full matrix — invocation method × N, with per-run payload hashes as above — rather than a summary.
What I can commit to is narrower and I would not want it oversold: the small-scale regression cell (3.7k, tight loop, N≥15) against any candidate build, purely as a check that a flag-parsing fix does not introduce a crash where none was observed before. It says nothing about the flaky rate.
Filed and I have commented there, because your description of our fleet inherited an error from my comment above: I claimed every business controller serves /api/v1/<resource>, and it does not — 26 of our 32 controllers carry no version segment, since URI versioning is per-controller with defaultVersion unset. Corrected inventory and the four concrete collision paths are in that thread.
The correction strengthens your precondition argument rather than weakening it: the strongest collision in our fleet is GET /health, declared in all four services and prefix-excluded in all four, so the path is byte-identical across four indexed projects. No overlapping domain, no migration relationship, no unusual convention — just the standard way to keep a health endpoint off a versioned prefix. Plus /api/projects and /api/aifunction, each declared unversioned in two independently-developed services.
I also asked that the issue state plainly that ours are predicted collisions read from decorators, not measured edges — #1428 masks them, so your 7/7 audit remains the only measured evidence. Better that than someone reproducing our side, finding total_cross_edges: 0, and concluding the example was inflated.
Your framing of why to cite an ordinary fleet over an unusual one is the right instinct, and it holds up better with the corrected facts than with mine.
Thank you for correcting the repeatability claim and recording both successes and crashes with identical payloads. The evidence supports a setup-dependent or nondeterministic worker failure, not a proven input-size threshold. The CLI target-array parsing, worker crash, and false service-owner matches are distinct contracts; the latter already has #1459. I have not localized the worker failure from this evidence, and neither a small successful run nor the zero-byte log establishes its cause.
Thank you @junk151516 and @HungChu. Keeping the flag-path no-op, the crash and the false-owner matches apart made this much easier to settle. Here is where each part stands:
--target-projects '["*"]' already parses as a JSON array on main (1a34255), so the documented flag form reaches the matcher; on a two-service fixture it reports projects_scanned: 2, the same as the stdin form.
The zero-targets case you both asked to make loud is fixed in fix(cross-repo): fail explicitly when zero target projects resolve (#1133) #2318: a target list that resolves to nothing but the source project, or ["*"] in a store with no other project, now exits non-zero with "resolved zero target projects", and it no longer wipes the existing cross-repo edges before noticing there is nothing to match.
Since v0.10.0, cross-repo mode runs in-process instead of inside the supervised indexing worker, so these requests can no longer produce the contained-crash envelope with a 0-byte log. If you see any crash on a current build, please send it and we'll follow it up separately.
The caller-project preference stays in #1459 (its correctness floor is now #2319), and your ordering point about landing it before new framework route extraction such as #1428 is followed.
Activity
DeusData commentedon Jul 16, 2026
Thanks for separating the flag-path no-op from the raw-JSON crash. This is a high-priority 0.9.1-rc correctness and containment bug. The RED cases should independently prove that
--target-projectspreserves a JSON array, zero resolved targets fail explicitly rather than returning success, and equivalent raw-JSON input cannot crash the worker. The existing project index must remain intact in every failure case; the 0-byte worker-log problem is tracked in #1132.HungChu commentedon Aug 4, 2026
Confirming Repro 1 on a different setup, and reporting a different outcome for Repro 2 — the stdin form does not crash here, which suggests your crash is orthogonal to the flag-parsing bug.
Environment: v0.9.0, Windows amd64, standard variant (you were on
ui), installed via npm.Minimal shareable repro — two throwaway TypeScript projects, ~50 LOC total, no
node_modulesneeded:Index both into a scratch cache, then:
projects_scanned: 0 → 2. Only the argument-passing form changed, so the flag path never reaches the matcher — same conclusion as yours, now also on the standard variant, on TS/JS instead of Python, and at ~48 nodes instead of ~31k. So it is neither language- nor scale-dependent.On your Repro 2: piping JSON on stdin is stable here — no worker crash in 7 invocations of the stdin form across 6 distinct projects: the two fixtures above (39 and 9 nodes) and four real TypeScript/NestJS/Next.js repos (516 / 614 / 3,289 / 3,678 nodes). All returned
"status":"success"withprojects_scanned≥ 2. So the stdin form looks like a usable workaround, and the crash you hit is likely a separate file-specific indexing failure rather than part of this bug (possibly the same root as #1379). Worth splitting so the parser fix is not blocked behind the crash.Why the silent-success part matters more than the parsing itself: our setup script had been running exactly the (A) form on every bootstrap and every repo-add for weeks.
projects_scanned: 0with"status":"success"and exit 0 meant the cross-repo pass had never once executed, and nothing in the output or exit code said so. We only found it while investigating why cross-repo edges were always empty — and the empty result was itself indistinguishable from "there genuinely are no cross-service calls". Strong +1 on erroring out when zero targets resolve; an agent consuming this output cannot tell success-with-no-work from success-with-work.(Separately:
--target-projects <array>… "Use["*"]for all indexed projects" incli index_repository --helpdocuments precisely the form that fails, so the help text points users at the broken path.)junk151516 commentedon Aug 4, 2026
@HungChu thanks — the Repro 1 corroboration is the useful kind, because it moves three axes at once relative to our report:
standardvariant vs ourui, TS/JS vs our Python, and ~48 nodes vs our ~31k. That removes variant, language and scale as explanations for the flag-path no-op. Agreed on splitting, and I'll add one reason below that got stronger since we filed.I can independently confirm your last point on the pinned v0.9.0
uibinary — the help text does document precisely the failing form:On Repro 2, one caveat about what your run establishes. I think your conclusion is probably right, but the measurement doesn't carry it yet. Your largest stdin invocation was 3,678 nodes; our crash was a ~31k-node source project against a ~13.5k-node target — about 8.5× above your maximum — and it reproduced 2 out of 2 attempts.
To be precise about what we saw, since it bears on the link to #1132: those two were contained worker crashes — the worker died, the existing graph database was left intact — not the OS-level freezes reported separately in that issue. What the two classes share is the 0-byte log signature: the worker is gone before a single line is flushed, which is what makes both of them RAM/IO-exhaustion candidates and why we filed them as one containment story rather than as the same event. A RAM-bound failure is scale-dependent by construction, so "no crash across 7 invocations at ≤3.7k nodes" is the expected observation whether the crash is orthogonal to the parser bug or sits in the same path.
Concretely falsifiable, if anyone wants to settle it: run the stdin form with a source project of ≥30k nodes against a target of ≥10k. If that returns
"status":"success"withprojects_scanned ≥ 2, orthogonality is established and #1132's containment work is genuinely unrelated to this issue. If it dies with a 0-byte log, the two are the same containment problem seen from different sides. I can run that side on Windows if it helps; I have the 31k/13.5k pair indexed.Worth being precise about one sentence, only because a reader skimming might over-apply it: "neither language- nor scale-dependent" holds for Repro 1, where you demonstrated it. Extending it to the crash would discount #1132's framing, which is the opposite of what your data shows.
The new argument for splitting: this issue has since moved from
0.9.1-rcto0.10.0-rc, along with #1132, #1196, #480, #1351 and #1206. The flag-parsing half is deterministic, has an obvious RED case, and needs no containment design — it does not need to wait on a crash whose reproduction is still contested. On #1379 as a possible shared root: plausible, but it is macOS, reports "all modes affected" rather than cross-repo specifically, and has been sitting inawaiting-reportersince 07-31. Tying the parser fix to it is a bad trade in either direction.Strong +1 on erroring out when zero targets resolve, and your framing of why is the part I'd want in the acceptance criteria. Our situation was the mirror image of yours: we caught it during a one-off validation on day one and blocked the mode in our indexing wrapper, so it never entered our pipeline. You ran the documented form on every bootstrap for weeks and the output could not tell you.
projects_scanned: 0+"status":"success"+ exit 0 is indistinguishable from "these services genuinely share no routes" — which is a plausible real answer, and that is exactly what makes the silent version dangerous. An agent consuming this cannot form the doubt that would make it check.junk151516 commentedon Aug 4, 2026
I ran the test I offered above, and the result corrects my own previous comment as much as it does anything else. Repro 2 is nondeterministic. Both our original "crashes 2/2" and @HungChu's "stable across 7 invocations" are samples of a flaky failure, reported by each of us as if it were deterministic.
Environment: v0.9.0, Windows amd64,
uivariant, pinned binary, pre-existing cache. Sourcekumo-app(31,287 nodes / 176,178 edges), targetwebIA(13,462 / 46,501) — i.e. ~8.5× the largest project in @HungChu's run, which was the scale gap I pointed at earlier.25 invocations, same binary, same store
["webIA"]["webIA"]["webIA"]["*"]For the byte-identical payload (
target_projects: ["webIA"]): 16 invocations → 9 success, 7 crash.So the crash does reproduce at ≥30k nodes — my scale hypothesis was wrong about the mechanism, though right that your run didn't settle the question.
What I ruled out, each with data rather than reasoning
["webIA"]both succeeds and crashes with an identical payload.The one correlation I could not explain away: every invocation launched from a tight foreground bash loop failed (13/13), while the backgrounded script and PowerShell succeeded 9 of 12. I am explicitly not claiming a mechanism there — with a ~56% per-call success rate, 13 consecutive failures is unlikely (p ≈ 0.001) but I could not isolate what differs beyond the loop itself.
Every failure returns the contained-crash envelope and leaves a 0-byte
logs/.worker-*.log— the same signature as #1132. I counted ten. The graph store survived all 16 crashes: 10 projects intact, node/edge counts stable,query_graphreturning correct rows throughout.The part that matters for the feature, not just the bug
When it succeeds, cross-repo-intelligence works. All 9 successful runs returned identical results — 117 cross-repo HTTP edges between the two projects — and they were idempotent: edge totals settled at 176,218 / 46,588 and did not inflate across repeated runs.
That changed our posture. We had blocked the entire mode in our indexing wrapper since July on the strength of a 2/2 crash. On this evidence the flag-path no-op is the part that is genuinely broken and silent; the stdin path is usable with retry, and we are unblocking it accordingly.
Suggested consequence for this issue
The split @HungChu proposed looks right, and the flakiness is an argument for it rather than against: the flag-parsing no-op is deterministic, has an obvious RED case, and is testable in isolation — whereas the crash needs a flaky-test harness (repeat N times, assert zero contained-crash envelopes) plus whatever makes the worker die without flushing a single log line. Blocking the first on the second means neither ships.
If it helps, I can run a fixed-N repetition harness against any candidate build and report the success/crash ratio, since I appear to have an environment that reproduces it about 4 times out of 10.
junk151516 commentedon Aug 4, 2026
Correction to my previous comment. I wrote that "when it succeeds, cross-repo-intelligence works," citing 117 edges as evidence. I checked those edges afterwards and that claim does not hold. The crash matrix stands; the working-feature claim does not.
What the edges actually are
Of the 40
CROSS_HTTP_CALLSwritten into the source project's graph, 3 point athttpbin.org/ip(health-check URLs — separate noise) and the remaining 37 span 20 distinct routes. I spot-checked 7 of those routes. Every one is defined inside the calling project itself.The most frequent target,
/api/proveedores, resolves cross-repo to the target project — while the caller's own backend contains:Same for
/api/clientes-externos,/api/facturas,/api/tasks,/api/empresas,/api/sedes,/api/horarios— each defined in 1–7 files of the caller's own backend. The callers are SvelteKit frontends living in the same repository as that backend, doingfetch('/api/proveedores')against their own API. There is no HTTP client to the other project at all: the config variable holding its base URL has zero consumers in the codebase.So these are path-string collisions, not cross-service calls. Our pair is close to a worst case for path-based matching — one project is the in-progress migration of the other, so they share route paths by construction. But the failure does not require that: it needs only two indexed projects that both expose a route with the same path, which is unremarkable for
/api/tasksor/oauth/token.The part I think is a real, separate bug
The matcher emits a cross-repo edge even when the caller's own project defines the same route. Nothing in the output distinguishes "this call demonstrably leaves the service" from "some other indexed project happens to expose this path too" — and the second is silently reported as the first.
That matters more than a wrong edge count, because the mode's output is the kind an agent consumes without a way to doubt it.
total_cross_edges: 117reads as an architectural fact.Two things that would fix it, in order of preference:
CALLSedges carryconfidence,candidatesandstrategy. ACROSS_HTTP_CALLSedge that hadcandidates: 2would let a consumer filter, instead of having to reverse-engineer route ownership by hand as I just did.Happy to open this as its own issue rather than let it ride here, given we have all been arguing for splitting this one — say the word and I will file it with the repro above.
What still stands from my previous comment
The nondeterminism is unchanged and is independent of this: 16 invocations of a byte-identical payload → 9 success / 7 contained crash, each failure leaving a 0-byte
logs/.worker-*.log. The successful runs were deterministic and idempotent in output — they consistently produced the same 117 edges and did not inflate on repetition. Those edges just do not mean what the field name says.HungChu commentedon Aug 5, 2026
@junk151516 your criticism of my Repro 2 inference was correct, and your 25-run matrix settles it. I over-read seven non-crashing runs at ≤3.7k nodes as evidence of orthogonality, when all it supported was "does not reproduce at this scale". Calling the crash "a separate file-specific indexing failure" was a claim my data could not carry — and pointing at #1379 was the weakest part, for the reasons you gave. Withdrawn.
Since a flaky failure is now the working model, I ran the one cell your matrix could not fill: the same tight foreground bash loop, at small scale. That was your single unexplained correlation (13/13 failures from a tight loop, 9/12 successes from backgrounded/PowerShell), and scale was the obvious confound.
15 invocations, tight foreground bash loop, byte-identical payload,
target_projects: ["*"]:projects_scanned: 4)Environment: v0.9.0, Windows amd64, standard variant, npm install, pre-existing shared cache, 5 indexed projects. Source project = the largest in our fleet, 3,678 nodes / 9,823 edges. No cooldown between calls; sequential in one
forloop in one shell.So the tight-loop correlation does not reproduce at 3.7k nodes. Combined with your data, the loop alone is not sufficient — something scale-dependent has to be present too, which is consistent with the RAM/IO-exhaustion framing in #1132 and inconsistent with "the loop is the mechanism". That narrows it in a useful direction, though obviously it cannot prove where the memory goes.
Graph integrity after the 15 runs: all 5 projects intact, node/edge counts unchanged on the source project (no edge inflation across repeated runs — matching your idempotence observation),
search_graphreturning correct rows. So the containment holds at this scale too.On your false-edge correction — please do file it separately, and I would treat it as the most consequential finding in this thread. Your pair shares route paths by construction, but as you say the precondition is only "two indexed projects both exposing the same path", and that is ordinary. Concretely: our fleet is four NestJS services behind a convention where every business controller serves
/api/v1/<resource>, with a shared Postgres and overlapping resource vocabulary between the service that admits records and the two that read them. Path-only matching would collide across those three by construction, not by accident.There is an ordering hazard worth naming in that new issue, because it is not obvious: I filed #1428 (NestJS
@Controller/@Getdecorators produce zeroRoutenodes). Today that gap makes our fleet immune to your false-edge bug — no Route nodes, no collisions,total_cross_edges: 0. If the extraction gap is fixed before route-ownership resolution, every affected fleet flips straight from "silently zero" to "silently wrong", with no intermediate state where anyone would notice. Zero edges at least looks like nothing happened; 117 plausible-looking edges read as an architectural fact. Your preference (1) — prefer the caller's own project, emit cross-repo only when the path is not defined locally — should probably land before or with any new framework's route extraction, not after.Your
candidates/confidencefallback also seems worth having regardless of (1), for the genuinely ambiguous case where two projects both define a path and the caller defines neither.I do not have a ≥30k-node project to contribute to the crash-repetition side, so I cannot help there — but if a candidate build lands, I can run the small-scale cell (3.7k, tight loop, N=15+) as a regression check that the flag-parsing fix does not introduce a crash at scales that were previously clean.
junk151516 commentedon Aug 5, 2026
@HungChu — appreciated, and the small-scale loop cell is a genuine addition. Three things: one correction to how it's labelled, one comparison from our matrix that I think you should have before the loop gets written off, and agreement on the ordering hazard, which I think is the most important thing either of us has posted in this thread.
Your cell establishes less than "scale-dependent", and more than you claimed elsewhere
The within-environment part of your test is clean and I have no quarrel with it: 7 non-loop and 15 loop invocations, same binary, same variant, same fleet, same 3,678-node source — no crash either way. Against the strong reading "the tight loop is the mechanism", that is decisive.
P(0 crashes in 15 | rate = 100%) = 0. That reading is dead and I am not defending it.What I do not think it supports is the label scale-dependent. The comparison that carries that conclusion is your setup against ours, and that varies on at least five axes at once:
uistandardThe defensible statement is setup-dependent, with scale as the leading candidate. I am pressing on this only because it is the same failure mode that has now bitten this thread three times — twice by me — and each time it looked exactly this reasonable: two or more variables moved, and the effect got assigned to the most interesting one.
One statistical footnote, since it affects how the cell should be cited later:
0/15bounds your crash rate at ≤18.1% (95%). It rules out 100%, and it rules out our observed 44%, but it does not establish zero. "Does not reproduce at 3.7k" should not harden into "3.7k is clean" in anyone's head, including mine.The comparison that discriminates is already in our matrix
You wrote that the loop correlation was the cell my matrix could not fill. It could not fill it at your scale — but the split at our scale is in the same 25 invocations:
So at 31k the invocation method has a large effect, and your 0/15 at 3.7k does not touch that. Taken together, the model consistent with both datasets is not "the loop is irrelevant" — it is an interaction: the loop matters above some threshold and not below it. Your phrasing ("inconsistent with 'the loop is the mechanism'") is strictly correct, but it reads as demoting the loop, and it is still the strongest single predictor either of us has. If it gets dropped as a variable in whatever harness runs against a candidate build, we lose the one lever that reliably moved the outcome.
p ≈ 1.1e-4as indicative, not as a clean controlled contrast. If someone reproduces this, log the payload hash per run — I did not, and that is why this caveat exists.The experiment that would actually settle it
You say you have no ≥30k-node project to contribute. You do not need one of yours. Index a large third-party TypeScript repo — the NestJS monorepo itself, the TypeScript compiler, Nx — until you clear ~30k nodes, then run the same tight loop on your own host with your own variant.
That converts the scale question from a cross-environment comparison into a ladder inside a single environment, which is worth considerably more than the cell you just filled, and it is the one design where scale is the only thing moving. If the loop crashes at 30k on your box, scale is confirmed and the variant/language axes are eliminated. If it does not, the difference is in our setup and I should go find it here.
Your framing of the small-scale regression cell is right, and worth keeping as-is: it checks that a flag-parsing fix does not introduce a crash where none existed. It cannot speak to the flaky rate, and you did not claim it could.
The ordering hazard — agreed, and it is not hypothetical
This is the part I would prioritise, and I want to add the piece that I think makes it land.
You present the flip from "silently zero" to "silently wrong" as what would happen if #1428 is fixed before route-ownership resolution. It is not conditional. Our two fleets are the two states of the same bug, right now:
Routenodes, no collisions:total_cross_edges: 0. Silently zero.total_cross_edges: 117. Of the 40CROSS_HTTP_CALLSattributed to kumo-app, 3 are obvious noise (httpbin.org/ip) and 37 are path collisions across 20 distinct routes. We hand-audited 7 of those routes: 7 of 7 are defined by the calling project itself, and 0 verified cross-service calls survived. Silently wrong, already shipped, for weeks.Nobody has to wait for #1428 to see the other side of the transition. We are standing on it. And we only caught it because a
CLAUDE.mdin that repo asserts there is no HTTP client to the legacy service, which contradicted the graph. Absent that contradiction sitting in the same repo, nobody audits a field calledtotal_cross_edges— it reads as an architectural fact, and nothing in the output distinguishes "this call leaves the service" from "another indexed project happens to expose the same path".One generalisation worth putting in the acceptance criteria: this is not a NestJS concern. Any improvement to route extraction, for any framework, flips every affected fleet from the first state to the second, with no intermediate state where a user would notice. So your preference (1) — resolve to the caller's own project, emit a cross-repo edge only when the path is not defined locally — is not a dependency of #1428 specifically. It is a precondition for the route-extraction roadmap as a whole.
Your
candidates/confidencefallback is worth having independently, for the genuinely ambiguous case where two projects define a path and the caller defines neither. The machinery already exists —CALLSedges carryconfidence/candidates/strategy— so this is plumbing an existing field through a new edge type, not new infrastructure.Filed as #1459, with the mechanism, our 7/7 audit, and the ordering argument credited to you.
I cited your fleet as the primary precondition example, and I want to be explicit about why rather than have it look like borrowing your evidence: our pair shares routes by construction — the source repo is the in-progress migration of the target — so it is close to a worst case for a path matcher, and it is easy to dismiss on exactly that basis. Four services behind
/api/v1/<resource>with an overlapping resource vocabulary is just an ordinary convention. "This happens to a normal fleet" is a much stronger argument than "this happens to our unusual one". Say the word if you would rather it be described differently and I will edit it.HungChu commentedon Aug 6, 2026
You are right on all three, and the labelling one is the same mistake you flagged earlier in this thread — I assigned the effect to the most interesting variable while five had moved. "Setup-dependent, with scale as the leading candidate" is the correct reading of my cell; I withdraw "scale-dependent". Two axes I had not even listed as differing: store size (your 10–11 indexed projects vs our 5) and variant.
Accepting the other two without qualification:
0/15bounds the crash rate at ≤18.1% (95%), it does not establish zero. It excludes 100% and excludes your observed 44%; that is all. I will cite it that way and not let it harden into "3.7k is clean".Your self-caveat about the split spanning all 25 rather than the byte-identical 16 is noted, and the lesson is worth stating for whoever reproduces this: log a payload hash per run. Your
pis indicative, and the reason it cannot be tightened is a logging gap, not a data gap.On the ladder experiment
The design is right, and it is clearly the experiment that discriminates — a single environment with scale as the only moving variable beats any cross-environment comparison, including mine. I am not going to promise a date for it: it means cloning a large third-party repo and driving an indexer to ~30k nodes on a corporate work machine whose network already blocks GitHub release assets, and that is a resource decision I do not own unilaterally. If I run it I will post the full matrix — invocation method × N, with per-run payload hashes as above — rather than a summary.
What I can commit to is narrower and I would not want it oversold: the small-scale regression cell (3.7k, tight loop, N≥15) against any candidate build, purely as a check that a flag-parsing fix does not introduce a crash where none was observed before. It says nothing about the flaky rate.
On #1459
Filed and I have commented there, because your description of our fleet inherited an error from my comment above: I claimed every business controller serves
/api/v1/<resource>, and it does not — 26 of our 32 controllers carry no version segment, since URI versioning is per-controller withdefaultVersionunset. Corrected inventory and the four concrete collision paths are in that thread.The correction strengthens your precondition argument rather than weakening it: the strongest collision in our fleet is
GET /health, declared in all four services and prefix-excluded in all four, so the path is byte-identical across four indexed projects. No overlapping domain, no migration relationship, no unusual convention — just the standard way to keep a health endpoint off a versioned prefix. Plus/api/projectsand/api/aifunction, each declared unversioned in two independently-developed services.I also asked that the issue state plainly that ours are predicted collisions read from decorators, not measured edges — #1428 masks them, so your 7/7 audit remains the only measured evidence. Better that than someone reproducing our side, finding
total_cross_edges: 0, and concluding the example was inflated.Your framing of why to cite an ordinary fleet over an unusual one is the right instinct, and it holds up better with the corrected facts than with mine.
fix(index): make a crashed worker's log survive and name the run
DeusData commentedon Sep 19, 2026
Thank you for correcting the repeatability claim and recording both successes and crashes with identical payloads. The evidence supports a setup-dependent or nondeterministic worker failure, not a proven input-size threshold. The CLI target-array parsing, worker crash, and false service-owner matches are distinct contracts; the latter already has #1459. I have not localized the worker failure from this evidence, and neither a small successful run nor the zero-byte log establishes its cause.
DeusData commentedon Sep 25, 2026
Thank you @junk151516 and @HungChu. Keeping the flag-path no-op, the crash and the false-owner matches apart made this much easier to settle. Here is where each part stands:
--target-projects '["*"]'already parses as a JSON array on main (1a34255), so the documented flag form reaches the matcher; on a two-service fixture it reportsprojects_scanned: 2, the same as the stdin form.["*"]in a store with no other project, now exits non-zero with "resolved zero target projects", and it no longer wipes the existing cross-repo edges before noticing there is nothing to match.The caller-project preference stays in #1459 (its correctness floor is now #2319), and your ordering point about landing it before new framework route extraction such as #1428 is followed.
Merge pull request #2318 from DeusData/fix/issue-1133
fix(cross-repo): fail explicitly when zero target projects resolve (D…
DeusData commentedon Sep 30, 2026
Thank you again for reporting this, @junk151516! The fix is now on
mainin aa53278 (merged via #2318), and it will ship in the next release.